chore: translate all Spanish comments and log messages to English
This commit is contained in:
@@ -44,12 +44,12 @@ impl From<File> for FileDto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Para convertir de FileDto a File para los batch handlers
|
// To convert from FileDto to File for batch handlers
|
||||||
impl From<FileDto> for File {
|
impl From<FileDto> for File {
|
||||||
fn from(dto: FileDto) -> Self {
|
fn from(dto: FileDto) -> Self {
|
||||||
// Usar constructor para crear una entidad desde DTO
|
// Use constructor to create an entity from DTO
|
||||||
// Nota: esto debe simplificarse si File tiene un constructor adecuado
|
// Note: this should be simplified if File has a proper constructor
|
||||||
// Si no, deberías hacer la conversión de la mejor manera posible
|
// If not, make the conversion as best as possible
|
||||||
File::from_dto(
|
File::from_dto(
|
||||||
dto.id,
|
dto.id,
|
||||||
dto.name,
|
dto.name,
|
||||||
|
|||||||
@@ -66,11 +66,11 @@ impl From<Folder> for FolderDto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Para convertir de FolderDto a Folder para los batch handlers
|
// To convert from FolderDto to Folder for batch handlers
|
||||||
impl From<FolderDto> for Folder {
|
impl From<FolderDto> for Folder {
|
||||||
fn from(dto: FolderDto) -> Self {
|
fn from(dto: FolderDto) -> Self {
|
||||||
// Usar constructor para crear una entidad desde DTO
|
// Use constructor to create an entity from DTO
|
||||||
// Nota: esto debe simplificarse si Folder tiene un constructor adecuado
|
// Note: this should be simplified if Folder has a proper constructor
|
||||||
Folder::from_dto(
|
Folder::from_dto(
|
||||||
dto.id,
|
dto.id,
|
||||||
dto.name,
|
dto.name,
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
/// Un DTO para representar información de paginación
|
/// A DTO to represent pagination information
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PaginationDto {
|
pub struct PaginationDto {
|
||||||
/// Página actual (comienza en 0)
|
/// Current page (starts at 0)
|
||||||
pub page: usize,
|
pub page: usize,
|
||||||
/// Tamaño de página
|
/// Page size
|
||||||
pub page_size: usize,
|
pub page_size: usize,
|
||||||
/// Número total de elementos
|
/// Total number of items
|
||||||
pub total_items: usize,
|
pub total_items: usize,
|
||||||
/// Número total de páginas
|
/// Total number of pages
|
||||||
pub total_pages: usize,
|
pub total_pages: usize,
|
||||||
/// Indica si hay una página siguiente
|
/// Indicates if there is a next page
|
||||||
pub has_next: bool,
|
pub has_next: bool,
|
||||||
/// Indica si hay una página anterior
|
/// Indicates if there is a previous page
|
||||||
pub has_prev: bool,
|
pub has_prev: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Un DTO para representar una solicitud de paginación
|
/// A DTO to represent a pagination request
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PaginationRequestDto {
|
pub struct PaginationRequestDto {
|
||||||
/// Página solicitada (comienza en 0)
|
/// Requested page (starts at 0)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub page: usize,
|
pub page: usize,
|
||||||
/// Tamaño de página solicitado
|
/// Requested page size
|
||||||
#[serde(default = "default_page_size")]
|
#[serde(default = "default_page_size")]
|
||||||
pub page_size: usize,
|
pub page_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Un DTO para representar una respuesta paginada
|
/// A DTO to represent a paginated response
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PaginatedResponseDto<T> {
|
pub struct PaginatedResponseDto<T> {
|
||||||
/// Datos en la página actual
|
/// Data on the current page
|
||||||
pub items: Vec<T>,
|
pub items: Vec<T>,
|
||||||
/// Información de paginación
|
/// Pagination information
|
||||||
pub pagination: PaginationDto,
|
pub pagination: PaginationDto,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,33 +46,33 @@ impl Default for PaginationRequestDto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Función para establecer el tamaño de página por defecto
|
/// Function to set the default page size
|
||||||
fn default_page_size() -> usize {
|
fn default_page_size() -> usize {
|
||||||
100 // Por defecto, 100 items por página
|
100 // By default, 100 items per page
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PaginationRequestDto {
|
impl PaginationRequestDto {
|
||||||
/// Calcula el offset para consultas paginadas
|
/// Calculates the offset for paginated queries
|
||||||
pub fn offset(&self) -> usize {
|
pub fn offset(&self) -> usize {
|
||||||
self.page * self.page_size
|
self.page * self.page_size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calcula el límite para consultas paginadas
|
/// Calculates the limit for paginated queries
|
||||||
pub fn limit(&self) -> usize {
|
pub fn limit(&self) -> usize {
|
||||||
self.page_size
|
self.page_size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Valida y ajusta los parámetros de paginación
|
/// Validates and adjusts the pagination parameters
|
||||||
pub fn validate_and_adjust(&self) -> Self {
|
pub fn validate_and_adjust(&self) -> Self {
|
||||||
let mut page = self.page;
|
let mut page = self.page;
|
||||||
let mut page_size = self.page_size;
|
let mut page_size = self.page_size;
|
||||||
|
|
||||||
// Asegurar que la página sea al menos 0
|
// Ensure the page is at least 0
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
page = 0;
|
page = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asegurar que el tamaño de página esté entre 10 y 500
|
// Ensure the page size is between 10 and 500
|
||||||
if page_size < 10 {
|
if page_size < 10 {
|
||||||
page_size = 10;
|
page_size = 10;
|
||||||
} else if page_size > 500 {
|
} else if page_size > 500 {
|
||||||
@@ -87,7 +87,7 @@ impl PaginationRequestDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T> PaginatedResponseDto<T> {
|
impl<T> PaginatedResponseDto<T> {
|
||||||
/// Crea una nueva respuesta paginada a partir de los datos y la información de paginación
|
/// Creates a new paginated response from the data and pagination information
|
||||||
pub fn new(
|
pub fn new(
|
||||||
items: Vec<T>,
|
items: Vec<T>,
|
||||||
page: usize,
|
page: usize,
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
/// DTO para elementos recientes
|
/// DTO for recent items
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RecentItemDto {
|
pub struct RecentItemDto {
|
||||||
/// Identificador único para el elemento reciente
|
/// Unique identifier for the recent item
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
||||||
/// ID del usuario propietario
|
/// Owner user ID
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
|
|
||||||
/// ID del elemento (archivo o carpeta)
|
/// Item ID (file or folder)
|
||||||
pub item_id: String,
|
pub item_id: String,
|
||||||
|
|
||||||
/// Tipo del elemento ('file' o 'folder')
|
/// Item type ('file' or 'folder')
|
||||||
pub item_type: String,
|
pub item_type: String,
|
||||||
|
|
||||||
/// Cuándo se accedió al elemento
|
/// When the item was accessed
|
||||||
pub accessed_at: DateTime<Utc>,
|
pub accessed_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ pub struct RefreshTokenDto {
|
|||||||
pub refresh_token: String,
|
pub refresh_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Datos del usuario autenticado actual (para uso en servicios de application)
|
/// Authenticated current user data (for use in application services)
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct CurrentUser {
|
pub struct CurrentUser {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|||||||
@@ -65,49 +65,49 @@ pub trait TokenServicePort: Send + Sync + 'static {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait UserStoragePort: Send + Sync + 'static {
|
pub trait UserStoragePort: Send + Sync + 'static {
|
||||||
/// Crea un nuevo usuario
|
/// Creates a new user
|
||||||
async fn create_user(&self, user: User) -> Result<User, DomainError>;
|
async fn create_user(&self, user: User) -> Result<User, DomainError>;
|
||||||
|
|
||||||
/// Obtiene un usuario por ID
|
/// Gets a user by ID
|
||||||
async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError>;
|
async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError>;
|
||||||
|
|
||||||
/// Obtiene un usuario por nombre de usuario
|
/// Gets a user by username
|
||||||
async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError>;
|
async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError>;
|
||||||
|
|
||||||
/// Obtiene un usuario por correo electrónico
|
/// Gets a user by email
|
||||||
async fn get_user_by_email(&self, email: &str) -> Result<User, DomainError>;
|
async fn get_user_by_email(&self, email: &str) -> Result<User, DomainError>;
|
||||||
|
|
||||||
/// Actualiza un usuario existente
|
/// Updates an existing user
|
||||||
async fn update_user(&self, user: User) -> Result<User, DomainError>;
|
async fn update_user(&self, user: User) -> Result<User, DomainError>;
|
||||||
|
|
||||||
/// Actualiza solo el uso de almacenamiento de un usuario
|
/// Updates only the storage usage of a user
|
||||||
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> Result<(), DomainError>;
|
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Lista usuarios con paginación
|
/// Lists users with pagination
|
||||||
async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<User>, DomainError>;
|
async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<User>, DomainError>;
|
||||||
|
|
||||||
/// Lista usuarios por rol (por ejemplo, "admin" o "user")
|
/// Lists users by role (e.g., "admin" or "user")
|
||||||
async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError>;
|
async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError>;
|
||||||
|
|
||||||
/// Elimina un usuario por su ID
|
/// Deletes a user by their ID
|
||||||
async fn delete_user(&self, user_id: &str) -> Result<(), DomainError>;
|
async fn delete_user(&self, user_id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Cambia la contraseña de un usuario
|
/// Changes a user's password
|
||||||
async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>;
|
async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Finds a user by OIDC provider + subject pair
|
/// Finds a user by OIDC provider + subject pair
|
||||||
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> Result<User, DomainError>;
|
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> Result<User, DomainError>;
|
||||||
|
|
||||||
/// Activa o desactiva un usuario
|
/// Activates or deactivates a user
|
||||||
async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError>;
|
async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Cambia el rol de un usuario
|
/// Changes a user's role
|
||||||
async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError>;
|
async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Actualiza la cuota de almacenamiento de un usuario
|
/// Updates a user's storage quota
|
||||||
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> Result<(), DomainError>;
|
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Cuenta el número total de usuarios
|
/// Counts the total number of users
|
||||||
async fn count_users(&self) -> Result<i64, DomainError>;
|
async fn count_users(&self) -> Result<i64, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,15 +156,15 @@ pub trait OidcServicePort: Send + Sync + 'static {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait SessionStoragePort: Send + Sync + 'static {
|
pub trait SessionStoragePort: Send + Sync + 'static {
|
||||||
/// Crea una nueva sesión
|
/// Creates a new session
|
||||||
async fn create_session(&self, session: Session) -> Result<Session, DomainError>;
|
async fn create_session(&self, session: Session) -> Result<Session, DomainError>;
|
||||||
|
|
||||||
/// Obtiene una sesión por token de actualización
|
/// Gets a session by refresh token
|
||||||
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> Result<Session, DomainError>;
|
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> Result<Session, DomainError>;
|
||||||
|
|
||||||
/// Revoca una sesión específica
|
/// Revokes a specific session
|
||||||
async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>;
|
async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Revoca todas las sesiones de un usuario
|
/// Revokes all sessions of a user
|
||||||
async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError>;
|
async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError>;
|
||||||
}
|
}
|
||||||
@@ -22,22 +22,22 @@ pub trait FavoritesUseCase: Send + Sync {
|
|||||||
// Outbound port — persistence abstraction
|
// Outbound port — persistence abstraction
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Puerto secundario (outbound) para persistencia de favoritos.
|
/// Secondary (outbound) port for favorites persistence.
|
||||||
///
|
///
|
||||||
/// Los servicios de aplicación dependen de este trait en lugar de
|
/// Application services depend on this trait instead of
|
||||||
/// acceder directamente a `PgPool`. La implementación concreta
|
/// accessing `PgPool` directly. The concrete implementation
|
||||||
/// vive en `infrastructure::repositories::pg`.
|
/// lives in `infrastructure::repositories::pg`.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FavoritesRepositoryPort: Send + Sync + 'static {
|
pub trait FavoritesRepositoryPort: Send + Sync + 'static {
|
||||||
/// Obtiene todos los favoritos de un usuario.
|
/// Gets all favorites for a user.
|
||||||
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>>;
|
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>>;
|
||||||
|
|
||||||
/// Añade un ítem a favoritos. Devuelve `Ok(())` si ya existía (idempotente).
|
/// Adds an item to favorites. Returns `Ok(())` if it already existed (idempotent).
|
||||||
async fn add_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
|
async fn add_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
|
||||||
|
|
||||||
/// Elimina un ítem de favoritos. Devuelve `true` si existía.
|
/// Removes an item from favorites. Returns `true` if it existed.
|
||||||
async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
||||||
|
|
||||||
/// Comprueba si un ítem está en favoritos.
|
/// Checks if an item is in favorites.
|
||||||
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
||||||
}
|
}
|
||||||
@@ -22,10 +22,10 @@ pub enum UploadStrategy {
|
|||||||
Streaming,
|
Streaming,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puerto primario para operaciones de subida de archivos
|
/// Primary port for file upload operations
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FileUploadUseCase: Send + Sync + 'static {
|
pub trait FileUploadUseCase: Send + Sync + 'static {
|
||||||
/// Sube un nuevo archivo desde bytes
|
/// Uploads a new file from bytes
|
||||||
async fn upload_file(
|
async fn upload_file(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -47,10 +47,10 @@ pub trait FileUploadUseCase: Send + Sync + 'static {
|
|||||||
total_size: usize,
|
total_size: usize,
|
||||||
) -> Result<(FileDto, UploadStrategy), DomainError>;
|
) -> Result<(FileDto, UploadStrategy), DomainError>;
|
||||||
|
|
||||||
/// Crea un nuevo archivo en la ruta especificada (para WebDAV)
|
/// Creates a new file at the specified path (for WebDAV)
|
||||||
async fn create_file(&self, parent_path: &str, filename: &str, content: &[u8], content_type: &str) -> Result<FileDto, DomainError>;
|
async fn create_file(&self, parent_path: &str, filename: &str, content: &[u8], content_type: &str) -> Result<FileDto, DomainError>;
|
||||||
|
|
||||||
/// Actualiza el contenido de un archivo existente (para WebDAV)
|
/// Updates the content of an existing file (for WebDAV)
|
||||||
async fn update_file(&self, path: &str, content: &[u8]) -> Result<(), DomainError>;
|
async fn update_file(&self, path: &str, content: &[u8]) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,22 +76,22 @@ pub enum OptimizedFileContent {
|
|||||||
Stream(Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>),
|
Stream(Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puerto primario para operaciones de recuperación de archivos
|
/// Primary port for file retrieval operations
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
||||||
/// Obtiene un archivo por su ID
|
/// Gets a file by its ID
|
||||||
async fn get_file(&self, id: &str) -> Result<FileDto, DomainError>;
|
async fn get_file(&self, id: &str) -> Result<FileDto, DomainError>;
|
||||||
|
|
||||||
/// Obtiene un archivo por su ruta (para WebDAV)
|
/// Gets a file by its path (for WebDAV)
|
||||||
async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>;
|
async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>;
|
||||||
|
|
||||||
/// Lista archivos en una carpeta
|
/// Lists files in a folder
|
||||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>;
|
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>;
|
||||||
|
|
||||||
/// Obtiene contenido de archivo como bytes (para archivos pequeños)
|
/// Gets file content as bytes (for small files)
|
||||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
||||||
|
|
||||||
/// Obtiene contenido de archivo como stream (para archivos grandes)
|
/// Gets file content as a stream (for large files)
|
||||||
async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||||
|
|
||||||
/// Optimized multi-tier download.
|
/// Optimized multi-tier download.
|
||||||
@@ -119,16 +119,16 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
|||||||
// Management port (delete, move)
|
// Management port (delete, move)
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Puerto primario para operaciones de gestión de archivos
|
/// Primary port for file management operations
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FileManagementUseCase: Send + Sync + 'static {
|
pub trait FileManagementUseCase: Send + Sync + 'static {
|
||||||
/// Mueve un archivo a otra carpeta
|
/// Moves a file to another folder
|
||||||
async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError>;
|
async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError>;
|
||||||
|
|
||||||
/// Renombra un archivo
|
/// Renames a file
|
||||||
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<FileDto, DomainError>;
|
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<FileDto, DomainError>;
|
||||||
|
|
||||||
/// Elimina un archivo
|
/// Deletes a file
|
||||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Smart delete: trash-first with dedup reference cleanup.
|
/// Smart delete: trash-first with dedup reference cleanup.
|
||||||
@@ -145,7 +145,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
|
|||||||
) -> Result<bool, DomainError>;
|
) -> Result<bool, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Factory para crear implementaciones de casos de uso de archivos
|
/// Factory for creating file use case implementations
|
||||||
pub trait FileUseCaseFactory: Send + Sync + 'static {
|
pub trait FileUseCaseFactory: Send + Sync + 'static {
|
||||||
fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase>;
|
fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase>;
|
||||||
fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase>;
|
fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase>;
|
||||||
|
|||||||
@@ -4,58 +4,58 @@ use crate::application::dtos::folder_dto::{CreateFolderDto, FolderDto, MoveFolde
|
|||||||
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
|
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
|
|
||||||
/// Puerto primario para operaciones de carpetas
|
/// Primary port for folder operations
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FolderUseCase: Send + Sync + 'static {
|
pub trait FolderUseCase: Send + Sync + 'static {
|
||||||
/// Crea una nueva carpeta
|
/// Creates a new folder
|
||||||
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError>;
|
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError>;
|
||||||
|
|
||||||
/// Obtiene una carpeta por su ID
|
/// Gets a folder by its ID
|
||||||
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError>;
|
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError>;
|
||||||
|
|
||||||
/// Obtiene una carpeta por su ruta
|
/// Gets a folder by its path
|
||||||
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>;
|
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>;
|
||||||
|
|
||||||
/// Lista carpetas dentro de una carpeta padre
|
/// Lists folders within a parent folder
|
||||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError>;
|
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError>;
|
||||||
|
|
||||||
/// Lista carpetas con paginación
|
/// Lists folders with pagination
|
||||||
async fn list_folders_paginated(
|
async fn list_folders_paginated(
|
||||||
&self,
|
&self,
|
||||||
parent_id: Option<&str>,
|
parent_id: Option<&str>,
|
||||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto
|
pagination: &crate::application::dtos::pagination::PaginationRequestDto
|
||||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
|
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
|
||||||
|
|
||||||
/// Renombra una carpeta
|
/// Renames a folder
|
||||||
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError>;
|
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError>;
|
||||||
|
|
||||||
/// Mueve una carpeta a otro padre
|
/// Moves a folder to another parent
|
||||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError>;
|
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError>;
|
||||||
|
|
||||||
/// Elimina una carpeta
|
/// Deletes a folder
|
||||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
|
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Puerto primario para búsqueda de archivos y carpetas
|
* Primary port for file and folder search
|
||||||
*
|
*
|
||||||
* Define las operaciones relacionadas con la búsqueda avanzada de
|
* Defines the operations related to advanced search of
|
||||||
* archivos y carpetas basándose en diversos criterios.
|
* files and folders based on various criteria.
|
||||||
*/
|
*/
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait SearchUseCase: Send + Sync + 'static {
|
pub trait SearchUseCase: Send + Sync + 'static {
|
||||||
/**
|
/**
|
||||||
* Realiza una búsqueda basada en los criterios especificados
|
* Performs a search based on the specified criteria
|
||||||
*
|
*
|
||||||
* @param criteria Criterios de búsqueda que incluyen texto, fechas, tamaños, etc.
|
* @param criteria Search criteria including text, dates, sizes, etc.
|
||||||
* @return Resultados de la búsqueda que contienen archivos y carpetas coincidentes
|
* @return Search results containing matching files and folders
|
||||||
*/
|
*/
|
||||||
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto, DomainError>;
|
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto, DomainError>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limpia la caché de resultados de búsqueda
|
* Clears the search results cache
|
||||||
*
|
*
|
||||||
* @return Resultado indicando éxito o error
|
* @return Result indicating success or error
|
||||||
*/
|
*/
|
||||||
async fn clear_search_cache(&self) -> Result<(), DomainError>;
|
async fn clear_search_cache(&self) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
@@ -9,71 +9,71 @@ pub use crate::domain::repositories::folder_repository::FolderRepository;
|
|||||||
|
|
||||||
use super::storage_ports::{FileReadPort, FileWritePort};
|
use super::storage_ports::{FileReadPort, FileWritePort};
|
||||||
|
|
||||||
/// Puerto secundario para operaciones de almacenamiento
|
/// Secondary port for storage operations
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait StoragePort: Send + Sync + 'static {
|
pub trait StoragePort: Send + Sync + 'static {
|
||||||
/// Resuelve una ruta de dominio a una ruta física
|
/// Resolves a domain path to a physical path
|
||||||
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf;
|
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf;
|
||||||
|
|
||||||
/// Crea directorios si no existen
|
/// Creates directories if they don't exist
|
||||||
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
|
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Verifica si existe un archivo en la ruta dada
|
/// Checks if a file exists at the given path
|
||||||
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||||
|
|
||||||
/// Verifica si existe un directorio en la ruta dada
|
/// Checks if a directory exists at the given path
|
||||||
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puerto unificado para persistencia de archivos (backward-compatible).
|
/// Unified port for file persistence (backward-compatible).
|
||||||
///
|
///
|
||||||
/// Ahora es un **supertrait** de `FileReadPort + FileWritePort`.
|
/// Now it is a **supertrait** of `FileReadPort + FileWritePort`.
|
||||||
/// Cualquier tipo que implemente ambos ports obtiene `FileStoragePort`
|
/// Any type that implements both ports gets `FileStoragePort`
|
||||||
/// automáticamente via blanket impl. Esto permite migrar consumidores
|
/// automatically via blanket impl. This allows consumers to be migrated
|
||||||
/// gradualmente a los ports granulares mientras los existentes siguen
|
/// gradually to granular ports while existing ones continue
|
||||||
/// funcionando sin cambios.
|
/// working without changes.
|
||||||
pub trait FileStoragePort: FileReadPort + FileWritePort {}
|
pub trait FileStoragePort: FileReadPort + FileWritePort {}
|
||||||
|
|
||||||
/// Blanket implementation: cualquier tipo que implemente ambos ports
|
/// Blanket implementation: any type that implements both ports
|
||||||
/// es automáticamente un FileStoragePort.
|
/// is automatically a FileStoragePort.
|
||||||
impl<T: FileReadPort + FileWritePort> FileStoragePort for T {}
|
impl<T: FileReadPort + FileWritePort> FileStoragePort for T {}
|
||||||
|
|
||||||
/// Puerto secundario para persistencia de carpetas (application layer).
|
/// Secondary port for folder persistence (application layer).
|
||||||
///
|
///
|
||||||
/// Tiene la misma firma que `FolderRepository` del dominio.
|
/// Has the same signature as the domain's `FolderRepository`.
|
||||||
/// Las implementaciones concretas deben implementar `FolderRepository`,
|
/// Concrete implementations must implement `FolderRepository`,
|
||||||
/// obteniendo `FolderStoragePort` automáticamente vía blanket impl.
|
/// getting `FolderStoragePort` automatically via blanket impl.
|
||||||
pub trait FolderStoragePort: FolderRepository {}
|
pub trait FolderStoragePort: FolderRepository {}
|
||||||
|
|
||||||
/// Blanket implementation: cualquier tipo que implemente FolderRepository
|
/// Blanket implementation: any type that implements FolderRepository
|
||||||
/// es automáticamente un FolderStoragePort.
|
/// is automatically a FolderStoragePort.
|
||||||
impl<T: FolderRepository> FolderStoragePort for T {}
|
impl<T: FolderRepository> FolderStoragePort for T {}
|
||||||
|
|
||||||
/// Puerto secundario para mapeo de IDs
|
/// Secondary port for ID mapping
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait IdMappingPort: Send + Sync + 'static {
|
pub trait IdMappingPort: Send + Sync + 'static {
|
||||||
/// Obtiene o crea un ID para una ruta
|
/// Gets or creates an ID for a path
|
||||||
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError>;
|
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError>;
|
||||||
|
|
||||||
/// Obtiene una ruta por su ID
|
/// Gets a path by its ID
|
||||||
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError>;
|
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||||
|
|
||||||
/// Actualiza la ruta para un ID existente
|
/// Updates the path for an existing ID
|
||||||
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError>;
|
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Elimina un ID del mapeo
|
/// Removes an ID from the mapping
|
||||||
async fn remove_id(&self, id: &str) -> Result<(), DomainError>;
|
async fn remove_id(&self, id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Guarda cambios pendientes
|
/// Saves pending changes
|
||||||
async fn save_changes(&self) -> Result<(), DomainError>;
|
async fn save_changes(&self) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Obtiene la ruta de archivo como PathBuf
|
/// Gets the file path as a PathBuf
|
||||||
async fn get_file_path(&self, file_id: &str) -> Result<PathBuf, DomainError> {
|
async fn get_file_path(&self, file_id: &str) -> Result<PathBuf, DomainError> {
|
||||||
let storage_path = self.get_path_by_id(file_id).await?;
|
let storage_path = self.get_path_by_id(file_id).await?;
|
||||||
Ok(PathBuf::from(storage_path.to_string()))
|
Ok(PathBuf::from(storage_path.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza la ruta de un archivo
|
/// Updates a file's path
|
||||||
async fn update_file_path(&self, file_id: &str, new_path: &PathBuf) -> Result<(), DomainError> {
|
async fn update_file_path(&self, file_id: &str, new_path: &PathBuf) -> Result<(), DomainError> {
|
||||||
let storage_path = StoragePath::from_string(&new_path.to_string_lossy().to_string());
|
let storage_path = StoragePath::from_string(&new_path.to_string_lossy().to_string());
|
||||||
self.update_path(file_id, &storage_path).await
|
self.update_path(file_id, &storage_path).await
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ use async_trait::async_trait;
|
|||||||
use crate::common::errors::Result;
|
use crate::common::errors::Result;
|
||||||
use crate::application::dtos::recent_dto::RecentItemDto;
|
use crate::application::dtos::recent_dto::RecentItemDto;
|
||||||
|
|
||||||
/// Define operaciones para gestionar elementos recientes del usuario
|
/// Defines operations for managing user recent items
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait RecentItemsUseCase: Send + Sync {
|
pub trait RecentItemsUseCase: Send + Sync {
|
||||||
/// Obtener todos los elementos recientes de un usuario
|
/// Get all recent items for a user
|
||||||
async fn get_recent_items(&self, user_id: &str, limit: Option<i32>) -> Result<Vec<RecentItemDto>>;
|
async fn get_recent_items(&self, user_id: &str, limit: Option<i32>) -> Result<Vec<RecentItemDto>>;
|
||||||
|
|
||||||
/// Registrar acceso a un elemento
|
/// Record access to an item
|
||||||
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
|
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
|
||||||
|
|
||||||
/// Eliminar un elemento de recientes
|
/// Remove an item from recents
|
||||||
async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
||||||
|
|
||||||
/// Limpiar toda la lista de elementos recientes
|
/// Clear the entire recent items list
|
||||||
async fn clear_recent_items(&self, user_id: &str) -> Result<()>;
|
async fn clear_recent_items(&self, user_id: &str) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,24 +22,24 @@ pub trait RecentItemsUseCase: Send + Sync {
|
|||||||
// Outbound port — persistence abstraction
|
// Outbound port — persistence abstraction
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Puerto secundario (outbound) para persistencia de elementos recientes.
|
/// Secondary (outbound) port for recent items persistence.
|
||||||
///
|
///
|
||||||
/// Abstrae el acceso a la tabla `auth.user_recent_files` para que
|
/// Abstracts access to the `auth.user_recent_files` table so that
|
||||||
/// `RecentService` no dependa directamente de `PgPool`.
|
/// `RecentService` does not depend directly on `PgPool`.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait RecentItemsRepositoryPort: Send + Sync + 'static {
|
pub trait RecentItemsRepositoryPort: Send + Sync + 'static {
|
||||||
/// Obtiene los últimos elementos recientes de un usuario (ordenados por fecha desc).
|
/// Gets the latest recent items for a user (ordered by date desc).
|
||||||
async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result<Vec<RecentItemDto>>;
|
async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result<Vec<RecentItemDto>>;
|
||||||
|
|
||||||
/// Registra/actualiza el acceso a un ítem (upsert por user+item+type).
|
/// Records/updates access to an item (upsert by user+item+type).
|
||||||
async fn upsert_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
|
async fn upsert_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
|
||||||
|
|
||||||
/// Elimina un ítem de recientes. Devuelve `true` si existía.
|
/// Removes an item from recents. Returns `true` if it existed.
|
||||||
async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
||||||
|
|
||||||
/// Elimina todos los elementos recientes de un usuario.
|
/// Removes all recent items for a user.
|
||||||
async fn clear_all(&self, user_id: &str) -> Result<()>;
|
async fn clear_all(&self, user_id: &str) -> Result<()>;
|
||||||
|
|
||||||
/// Elimina elementos que excedan `max_items` (los más antiguos).
|
/// Removes items exceeding `max_items` (the oldest ones).
|
||||||
async fn prune(&self, user_id: &str, max_items: i32) -> Result<()>;
|
async fn prune(&self, user_id: &str, max_items: i32) -> Result<()>;
|
||||||
}
|
}
|
||||||
@@ -17,28 +17,28 @@ pub use crate::domain::repositories::folder_repository::FolderRepository;
|
|||||||
// FileReadPort — application-layer alias for FileReadRepository
|
// FileReadPort — application-layer alias for FileReadRepository
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Puerto secundario para **lectura** de archivos.
|
/// Secondary port for file **reading**.
|
||||||
///
|
///
|
||||||
/// Encapsula toda operación que consulta estado sin modificarlo:
|
/// Encapsulates every operation that queries state without modifying it:
|
||||||
/// get, list, content, stream, mmap, range, resolución de rutas.
|
/// get, list, content, stream, mmap, range, path resolution.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FileReadPort: Send + Sync + 'static {
|
pub trait FileReadPort: Send + Sync + 'static {
|
||||||
/// Obtiene un archivo por su ID.
|
/// Gets a file by its ID.
|
||||||
async fn get_file(&self, id: &str) -> Result<File, DomainError>;
|
async fn get_file(&self, id: &str) -> Result<File, DomainError>;
|
||||||
|
|
||||||
/// Lista archivos en una carpeta.
|
/// Lists files in a folder.
|
||||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>;
|
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>;
|
||||||
|
|
||||||
/// Obtiene contenido completo como bytes (solo archivos pequeños/medianos).
|
/// Gets the full content as bytes (small/medium files only).
|
||||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
||||||
|
|
||||||
/// Obtiene contenido como stream (ideal para archivos grandes).
|
/// Gets content as a stream (ideal for large files).
|
||||||
async fn get_file_stream(
|
async fn get_file_stream(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||||
|
|
||||||
/// Stream de un rango de bytes (HTTP Range Requests, video seek).
|
/// Stream of a byte range (HTTP Range Requests, video seek).
|
||||||
async fn get_file_range_stream(
|
async fn get_file_range_stream(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
@@ -46,13 +46,13 @@ pub trait FileReadPort: Send + Sync + 'static {
|
|||||||
end: Option<u64>,
|
end: Option<u64>,
|
||||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||||
|
|
||||||
/// Memory-map de archivo para acceso zero-copy (10–100 MB).
|
/// Memory-map of a file for zero-copy access (10–100 MB).
|
||||||
async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError>;
|
async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError>;
|
||||||
|
|
||||||
/// Obtiene la ruta de almacenamiento lógica de un archivo.
|
/// Gets the logical storage path of a file.
|
||||||
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||||
|
|
||||||
/// Obtiene el ID de la carpeta padre a partir de una ruta (WebDAV).
|
/// Gets the parent folder ID from a path (WebDAV).
|
||||||
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError>;
|
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,13 +60,13 @@ pub trait FileReadPort: Send + Sync + 'static {
|
|||||||
// FileWritePort — all write / mutate operations
|
// FileWritePort — all write / mutate operations
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Puerto secundario para **escritura** de archivos.
|
/// Secondary port for file **writing**.
|
||||||
///
|
///
|
||||||
/// Cubre: upload (buffered + streaming), move, delete, update,
|
/// Covers: upload (buffered + streaming), move, delete, update,
|
||||||
/// y el registro diferido para write-behind cache.
|
/// and deferred registration for the write-behind cache.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FileWritePort: Send + Sync + 'static {
|
pub trait FileWritePort: Send + Sync + 'static {
|
||||||
/// Guarda un nuevo archivo desde bytes.
|
/// Saves a new file from bytes.
|
||||||
async fn save_file(
|
async fn save_file(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -75,7 +75,7 @@ pub trait FileWritePort: Send + Sync + 'static {
|
|||||||
content: Vec<u8>,
|
content: Vec<u8>,
|
||||||
) -> Result<File, DomainError>;
|
) -> Result<File, DomainError>;
|
||||||
|
|
||||||
/// Upload en streaming — escribe chunks a disco sin acumular en RAM.
|
/// Streaming upload — writes chunks to disk without accumulating in RAM.
|
||||||
async fn save_file_from_stream(
|
async fn save_file_from_stream(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -84,30 +84,30 @@ pub trait FileWritePort: Send + Sync + 'static {
|
|||||||
stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
||||||
) -> Result<File, DomainError>;
|
) -> Result<File, DomainError>;
|
||||||
|
|
||||||
/// Mueve un archivo a otra carpeta.
|
/// Moves a file to another folder.
|
||||||
async fn move_file(
|
async fn move_file(
|
||||||
&self,
|
&self,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
target_folder_id: Option<String>,
|
target_folder_id: Option<String>,
|
||||||
) -> Result<File, DomainError>;
|
) -> Result<File, DomainError>;
|
||||||
|
|
||||||
/// Renombra un archivo (same folder, different name).
|
/// Renames a file (same folder, different name).
|
||||||
async fn rename_file(
|
async fn rename_file(
|
||||||
&self,
|
&self,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
new_name: &str,
|
new_name: &str,
|
||||||
) -> Result<File, DomainError>;
|
) -> Result<File, DomainError>;
|
||||||
|
|
||||||
/// Elimina un archivo.
|
/// Deletes a file.
|
||||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Actualiza el contenido de un archivo existente.
|
/// Updates the content of an existing file.
|
||||||
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError>;
|
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Registra metadatos de archivo SIN escribir contenido a disco (write-behind).
|
/// Registers file metadata WITHOUT writing content to disk (write-behind).
|
||||||
///
|
///
|
||||||
/// Devuelve `(File, PathBuf)` donde `PathBuf` es la ruta destino para la
|
/// Returns `(File, PathBuf)` where `PathBuf` is the destination path for the
|
||||||
/// escritura diferida que realizará el `WriteBehindCache`.
|
/// deferred write that the `WriteBehindCache` will perform.
|
||||||
async fn register_file_deferred(
|
async fn register_file_deferred(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -118,13 +118,13 @@ pub trait FileWritePort: Send + Sync + 'static {
|
|||||||
|
|
||||||
// ── Trash operations ──
|
// ── Trash operations ──
|
||||||
|
|
||||||
/// Mueve un archivo a la papelera
|
/// Moves a file to the trash
|
||||||
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
|
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Restaura un archivo desde la papelera a su ubicación original
|
/// Restores a file from the trash to its original location
|
||||||
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>;
|
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Elimina un archivo permanentemente (usado por la papelera)
|
/// Permanently deletes a file (used by the trash)
|
||||||
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
|
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,40 +132,40 @@ pub trait FileWritePort: Send + Sync + 'static {
|
|||||||
// Auxiliary ports (unchanged)
|
// Auxiliary ports (unchanged)
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Puerto secundario para resolución de rutas de archivos
|
/// Secondary port for file path resolution
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FilePathResolutionPort: Send + Sync + 'static {
|
pub trait FilePathResolutionPort: Send + Sync + 'static {
|
||||||
/// Obtiene la ruta de almacenamiento de un archivo
|
/// Gets the storage path of a file
|
||||||
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||||
|
|
||||||
/// Resuelve una ruta de dominio a una ruta física
|
/// Resolves a domain path to a physical path
|
||||||
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf;
|
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puerto secundario para verificación de existencia de archivos/directorios
|
/// Secondary port for file/directory existence verification
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait StorageVerificationPort: Send + Sync + 'static {
|
pub trait StorageVerificationPort: Send + Sync + 'static {
|
||||||
/// Verifica si existe un archivo en la ruta dada
|
/// Checks whether a file exists at the given path
|
||||||
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||||
|
|
||||||
/// Verifica si existe un directorio en la ruta dada
|
/// Checks whether a directory exists at the given path
|
||||||
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puerto secundario para gestión de directorios
|
/// Secondary port for directory management
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait DirectoryManagementPort: Send + Sync + 'static {
|
pub trait DirectoryManagementPort: Send + Sync + 'static {
|
||||||
/// Crea directorios si no existen
|
/// Creates directories if they do not exist
|
||||||
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
|
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puerto secundario para gestión de uso de almacenamiento
|
/// Secondary port for storage usage management
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait StorageUsagePort: Send + Sync + 'static {
|
pub trait StorageUsagePort: Send + Sync + 'static {
|
||||||
/// Actualiza estadísticas de uso de almacenamiento para un usuario
|
/// Updates storage usage statistics for a user
|
||||||
async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError>;
|
async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError>;
|
||||||
|
|
||||||
/// Actualiza estadísticas de uso de almacenamiento para todos los usuarios
|
/// Updates storage usage statistics for all users
|
||||||
async fn update_all_users_storage_usage(&self) -> Result<(), DomainError>;
|
async fn update_all_users_storage_usage(&self) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,13 +68,13 @@ impl AuthApplicationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configura el servicio de carpetas, necesario para crear carpetas personales
|
/// Configures the folder service, needed to create personal folders
|
||||||
pub fn with_folder_service(mut self, folder_service: Arc<dyn FolderUseCase>) -> Self {
|
pub fn with_folder_service(mut self, folder_service: Arc<dyn FolderUseCase>) -> Self {
|
||||||
self.folder_service = Some(folder_service);
|
self.folder_service = Some(folder_service);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configura el servicio OIDC
|
/// Configures the OIDC service
|
||||||
pub fn with_oidc(self, oidc_service: Arc<dyn OidcServicePort>, oidc_config: OidcConfig) -> Self {
|
pub fn with_oidc(self, oidc_service: Arc<dyn OidcServicePort>, oidc_config: OidcConfig) -> Self {
|
||||||
{
|
{
|
||||||
let mut state = self.oidc.write().unwrap();
|
let mut state = self.oidc.write().unwrap();
|
||||||
@@ -123,12 +123,12 @@ impl AuthApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register(&self, dto: RegisterDto) -> Result<UserDto, DomainError> {
|
pub async fn register(&self, dto: RegisterDto) -> Result<UserDto, DomainError> {
|
||||||
// Verificar usuario duplicado
|
// Check for duplicate user
|
||||||
if self.user_storage.get_user_by_username(&dto.username).await.is_ok() {
|
if self.user_storage.get_user_by_username(&dto.username).await.is_ok() {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AlreadyExists,
|
ErrorKind::AlreadyExists,
|
||||||
"User",
|
"User",
|
||||||
format!("El usuario '{}' ya existe", dto.username)
|
format!("User '{}' already exists", dto.username)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,62 +136,62 @@ impl AuthApplicationService {
|
|||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AlreadyExists,
|
ErrorKind::AlreadyExists,
|
||||||
"User",
|
"User",
|
||||||
format!("El email '{}' ya está registrado", dto.email)
|
format!("Email '{}' is already registered", dto.email)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar si el usuario quiere crear un admin
|
// Check if the user wants to create an admin
|
||||||
let is_admin_request = dto.username.to_lowercase() == "admin" ||
|
let is_admin_request = dto.username.to_lowercase() == "admin" ||
|
||||||
(dto.role.is_some() && dto.role.as_ref().unwrap().to_lowercase() == "admin");
|
(dto.role.is_some() && dto.role.as_ref().unwrap().to_lowercase() == "admin");
|
||||||
|
|
||||||
// Si está intentando crear un admin, verificar si ya existen admins en el sistema
|
// If trying to create an admin, check if admins already exist in the system
|
||||||
if is_admin_request {
|
if is_admin_request {
|
||||||
match self.count_admin_users().await {
|
match self.count_admin_users().await {
|
||||||
Ok(admin_count) => {
|
Ok(admin_count) => {
|
||||||
// Si ya hay admins en el sistema y no estamos en instalación limpia,
|
// If there are already admins in the system and this is not a clean install,
|
||||||
// no permitimos crear otro admin desde el registro
|
// we do not allow creating another admin from registration
|
||||||
if admin_count > 0 {
|
if admin_count > 0 {
|
||||||
// Verificar si es una instalación limpia (solo el admin predeterminado)
|
// Check if this is a clean install (only the default admin)
|
||||||
match self.count_all_users().await {
|
match self.count_all_users().await {
|
||||||
Ok(user_count) => {
|
Ok(user_count) => {
|
||||||
// Si hay más de 2 usuarios (admin + test), no es instalación limpia
|
// If there are more than 2 users (admin + test), it is not a clean install
|
||||||
if user_count > 2 {
|
if user_count > 2 {
|
||||||
tracing::warn!("Intento de crear admin adicional rechazado: ya existe al menos un admin");
|
tracing::warn!("Attempt to create additional admin rejected: at least one admin already exists");
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"User",
|
"User",
|
||||||
"No se permite crear usuarios admin adicionales desde la página de registro"
|
"Creating additional admin users from the registration page is not allowed"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// En caso contrario, es instalación limpia y se permite el primer admin
|
// Otherwise, it is a clean install and the first admin is allowed
|
||||||
tracing::info!("Permitiendo creación de admin en instalación limpia");
|
tracing::info!("Allowing admin creation on clean install");
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Error al contar usuarios: {}", e);
|
tracing::error!("Error counting users: {}", e);
|
||||||
// Por seguridad, si no podemos verificar, rechazamos la creación de admin
|
// For security, if we cannot verify, we reject admin creation
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"User",
|
"User",
|
||||||
"No se permite crear usuarios admin adicionales"
|
"Creating additional admin users is not allowed"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Error al contar usuarios admin: {}", e);
|
tracing::error!("Error counting admin users: {}", e);
|
||||||
// Por seguridad, si no podemos verificar, rechazamos la creación de admin
|
// For security, if we cannot verify, we reject admin creation
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"User",
|
"User",
|
||||||
"No se permite crear usuarios admin adicionales"
|
"Creating additional admin users is not allowed"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determinar rol y cuota según el tipo de usuario
|
// Determine role and quota based on user type
|
||||||
// Si se proporciona un rol explícito de "admin", usar rol de administrador
|
// If an explicit "admin" role is provided, use the administrator role
|
||||||
let role = if let Some(role_str) = &dto.role {
|
let role = if let Some(role_str) = &dto.role {
|
||||||
if role_str.to_lowercase() == "admin" {
|
if role_str.to_lowercase() == "admin" {
|
||||||
UserRole::Admin
|
UserRole::Admin
|
||||||
@@ -199,7 +199,7 @@ impl AuthApplicationService {
|
|||||||
UserRole::User
|
UserRole::User
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Caso especial: si el nombre es "admin", asignar rol de admin aunque no se especifique
|
// Special case: if the username is "admin", assign admin role even if not specified
|
||||||
if dto.username.to_lowercase() == "admin" {
|
if dto.username.to_lowercase() == "admin" {
|
||||||
UserRole::Admin
|
UserRole::Admin
|
||||||
} else {
|
} else {
|
||||||
@@ -207,26 +207,26 @@ impl AuthApplicationService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cuota según el rol: 100GB para admin, 1GB para usuarios normales
|
// Quota based on role: 100GB for admin, 1GB for regular users
|
||||||
let quota = if role == UserRole::Admin {
|
let quota = if role == UserRole::Admin {
|
||||||
107374182400 // 100GB para admin
|
107374182400 // 100GB for admin
|
||||||
} else {
|
} else {
|
||||||
1024 * 1024 * 1024 // 1GB para usuarios normales
|
1024 * 1024 * 1024 // 1GB for regular users
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validar longitud de password antes de hashear
|
// Validate password length before hashing
|
||||||
if dto.password.len() < 8 {
|
if dto.password.len() < 8 {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::InvalidInput,
|
ErrorKind::InvalidInput,
|
||||||
"User",
|
"User",
|
||||||
"Password debe tener al menos 8 caracteres"
|
"Password must be at least 8 characters long"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hashear el password usando el servicio de infraestructura
|
// Hash the password using the infrastructure service
|
||||||
let password_hash = self.password_hasher.hash_password(&dto.password)?;
|
let password_hash = self.password_hasher.hash_password(&dto.password)?;
|
||||||
|
|
||||||
// Crear usuario con el hash pre-generado
|
// Create user with the pre-generated hash
|
||||||
let user = User::new(
|
let user = User::new(
|
||||||
dto.username.clone(),
|
dto.username.clone(),
|
||||||
dto.email,
|
dto.email,
|
||||||
@@ -236,13 +236,13 @@ impl AuthApplicationService {
|
|||||||
).map_err(|e| DomainError::new(
|
).map_err(|e| DomainError::new(
|
||||||
ErrorKind::InvalidInput,
|
ErrorKind::InvalidInput,
|
||||||
"User",
|
"User",
|
||||||
format!("Error al crear usuario: {}", e)
|
format!("Error creating user: {}", e)
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
// Guardar usuario
|
// Save user
|
||||||
let created_user = self.user_storage.create_user(user).await?;
|
let created_user = self.user_storage.create_user(user).await?;
|
||||||
|
|
||||||
// Crear carpeta personal para el usuario
|
// Create personal folder for the user
|
||||||
if let Some(folder_service) = &self.folder_service {
|
if let Some(folder_service) = &self.folder_service {
|
||||||
let folder_name = format!("Mi Carpeta - {}", dto.username);
|
let folder_name = format!("Mi Carpeta - {}", dto.username);
|
||||||
|
|
||||||
@@ -252,20 +252,20 @@ impl AuthApplicationService {
|
|||||||
}).await {
|
}).await {
|
||||||
Ok(folder) => {
|
Ok(folder) => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Carpeta personal creada para el usuario {}: {} (ID: {})",
|
"Personal folder created for user {}: {} (ID: {})",
|
||||||
created_user.id(),
|
created_user.id(),
|
||||||
folder.name,
|
folder.name,
|
||||||
folder.id
|
folder.id
|
||||||
);
|
);
|
||||||
|
|
||||||
// Aquí se podría guardar la asociación de la carpeta al usuario
|
// Here we could save the folder-to-user association,
|
||||||
// por ejemplo, en una tabla de relación carpeta-usuario
|
// for example, in a folder-user relationship table
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// No fallamos el registro por un error en la creación de la carpeta
|
// We don't fail registration due to a folder creation error,
|
||||||
// pero lo registramos para investigación
|
// but we log it for investigation
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"No se pudo crear la carpeta personal para el usuario {}: {}",
|
"Could not create personal folder for user {}: {}",
|
||||||
created_user.id(),
|
created_user.id(),
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
@@ -273,67 +273,67 @@ impl AuthApplicationService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"No se configuró el servicio de carpetas, no se puede crear carpeta personal para el usuario: {}",
|
"Folder service not configured, cannot create personal folder for user: {}",
|
||||||
created_user.id()
|
created_user.id()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!("Usuario registrado: {}", created_user.id());
|
tracing::info!("User registered: {}", created_user.id());
|
||||||
Ok(UserDto::from(created_user))
|
Ok(UserDto::from(created_user))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(&self, dto: LoginDto) -> Result<AuthResponseDto, DomainError> {
|
pub async fn login(&self, dto: LoginDto) -> Result<AuthResponseDto, DomainError> {
|
||||||
// Buscar usuario
|
// Find user
|
||||||
let mut user = self.user_storage
|
let mut user = self.user_storage
|
||||||
.get_user_by_username(&dto.username)
|
.get_user_by_username(&dto.username)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| DomainError::new(
|
.map_err(|_| DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"Auth",
|
"Auth",
|
||||||
"Credenciales inválidas"
|
"Invalid credentials"
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
// Verificar si usuario está activo
|
// Check if user is active
|
||||||
if !user.is_active() {
|
if !user.is_active() {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"Auth",
|
"Auth",
|
||||||
"Cuenta desactivada"
|
"Account deactivated"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar contraseña usando el hasher inyectado
|
// Verify password using the injected hasher
|
||||||
let is_valid = self.password_hasher.verify_password(&dto.password, user.password_hash())?;
|
let is_valid = self.password_hasher.verify_password(&dto.password, user.password_hash())?;
|
||||||
|
|
||||||
if !is_valid {
|
if !is_valid {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"Auth",
|
"Auth",
|
||||||
"Credenciales inválidas"
|
"Invalid credentials"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar último login
|
// Update last login
|
||||||
user.register_login();
|
user.register_login();
|
||||||
self.user_storage.update_user(user.clone()).await?;
|
self.user_storage.update_user(user.clone()).await?;
|
||||||
|
|
||||||
// Generar tokens usando el servicio de tokens inyectado
|
// Generate tokens using the injected token service
|
||||||
let access_token = self.token_service.generate_access_token(&user)?;
|
let access_token = self.token_service.generate_access_token(&user)?;
|
||||||
|
|
||||||
let refresh_token = self.token_service.generate_refresh_token();
|
let refresh_token = self.token_service.generate_refresh_token();
|
||||||
|
|
||||||
// Guardar sesión
|
// Save session
|
||||||
let session = Session::new(
|
let session = Session::new(
|
||||||
user.id().to_string(),
|
user.id().to_string(),
|
||||||
refresh_token.clone(),
|
refresh_token.clone(),
|
||||||
None, // IP (se puede añadir desde la capa HTTP)
|
None, // IP (can be added from the HTTP layer)
|
||||||
None, // User-Agent (se puede añadir desde la capa HTTP)
|
None, // User-Agent (can be added from the HTTP layer)
|
||||||
self.token_service.refresh_token_expiry_days(),
|
self.token_service.refresh_token_expiry_days(),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.session_storage.create_session(session).await?;
|
self.session_storage.create_session(session).await?;
|
||||||
|
|
||||||
// Respuesta de autenticación
|
// Authentication response
|
||||||
Ok(AuthResponseDto {
|
Ok(AuthResponseDto {
|
||||||
user: UserDto::from(user),
|
user: UserDto::from(user),
|
||||||
access_token,
|
access_token,
|
||||||
@@ -344,43 +344,43 @@ impl AuthApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh_token(&self, dto: RefreshTokenDto) -> Result<AuthResponseDto, DomainError> {
|
pub async fn refresh_token(&self, dto: RefreshTokenDto) -> Result<AuthResponseDto, DomainError> {
|
||||||
// Obtener sesión válida
|
// Get valid session
|
||||||
let session = self.session_storage
|
let session = self.session_storage
|
||||||
.get_session_by_refresh_token(&dto.refresh_token)
|
.get_session_by_refresh_token(&dto.refresh_token)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Verificar si la sesión está expirada o revocada
|
// Check if the session is expired or revoked
|
||||||
if session.is_expired() || session.is_revoked() {
|
if session.is_expired() || session.is_revoked() {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"Auth",
|
"Auth",
|
||||||
"Sesión expirada o inválida"
|
"Session expired or invalid"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtener usuario
|
// Get user
|
||||||
let user = self.user_storage
|
let user = self.user_storage
|
||||||
.get_user_by_id(session.user_id())
|
.get_user_by_id(session.user_id())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Verificar si usuario está activo
|
// Check if user is active
|
||||||
if !user.is_active() {
|
if !user.is_active() {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"Auth",
|
"Auth",
|
||||||
"Cuenta desactivada"
|
"Account deactivated"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revocar sesión actual
|
// Revoke current session
|
||||||
self.session_storage.revoke_session(session.id()).await?;
|
self.session_storage.revoke_session(session.id()).await?;
|
||||||
|
|
||||||
// Generar nuevos tokens
|
// Generate new tokens
|
||||||
let access_token = self.token_service.generate_access_token(&user)?;
|
let access_token = self.token_service.generate_access_token(&user)?;
|
||||||
|
|
||||||
let new_refresh_token = self.token_service.generate_refresh_token();
|
let new_refresh_token = self.token_service.generate_refresh_token();
|
||||||
|
|
||||||
// Crear nueva sesión
|
// Create new session
|
||||||
let new_session = Session::new(
|
let new_session = Session::new(
|
||||||
user.id().to_string(),
|
user.id().to_string(),
|
||||||
new_refresh_token.clone(),
|
new_refresh_token.clone(),
|
||||||
@@ -401,67 +401,67 @@ impl AuthApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn logout(&self, user_id: &str, refresh_token: &str) -> Result<(), DomainError> {
|
pub async fn logout(&self, user_id: &str, refresh_token: &str) -> Result<(), DomainError> {
|
||||||
// Obtener sesión
|
// Get session
|
||||||
let session = match self.session_storage.get_session_by_refresh_token(refresh_token).await {
|
let session = match self.session_storage.get_session_by_refresh_token(refresh_token).await {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
// Si la sesión no existe, consideramos el logout como exitoso
|
// If the session doesn't exist, we consider the logout successful
|
||||||
Err(_) => return Ok(()),
|
Err(_) => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verificar que la sesión pertenece al usuario
|
// Verify that the session belongs to the user
|
||||||
if session.user_id() != user_id {
|
if session.user_id() != user_id {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"Auth",
|
"Auth",
|
||||||
"La sesión no pertenece al usuario"
|
"The session does not belong to the user"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revocar sesión
|
// Revoke session
|
||||||
self.session_storage.revoke_session(session.id()).await?;
|
self.session_storage.revoke_session(session.id()).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn logout_all(&self, user_id: &str) -> Result<u64, DomainError> {
|
pub async fn logout_all(&self, user_id: &str) -> Result<u64, DomainError> {
|
||||||
// Revocar todas las sesiones del usuario
|
// Revoke all user sessions
|
||||||
let revoked_count = self.session_storage.revoke_all_user_sessions(user_id).await?;
|
let revoked_count = self.session_storage.revoke_all_user_sessions(user_id).await?;
|
||||||
|
|
||||||
Ok(revoked_count)
|
Ok(revoked_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn change_password(&self, user_id: &str, dto: ChangePasswordDto) -> Result<(), DomainError> {
|
pub async fn change_password(&self, user_id: &str, dto: ChangePasswordDto) -> Result<(), DomainError> {
|
||||||
// Obtener usuario
|
// Get user
|
||||||
let mut user = self.user_storage.get_user_by_id(user_id).await?;
|
let mut user = self.user_storage.get_user_by_id(user_id).await?;
|
||||||
|
|
||||||
// Verificar contraseña actual usando el hasher inyectado
|
// Verify current password using the injected hasher
|
||||||
let is_valid = self.password_hasher.verify_password(&dto.current_password, user.password_hash())?;
|
let is_valid = self.password_hasher.verify_password(&dto.current_password, user.password_hash())?;
|
||||||
|
|
||||||
if !is_valid {
|
if !is_valid {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"Auth",
|
"Auth",
|
||||||
"Contraseña actual incorrecta"
|
"Current password is incorrect"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validar nueva contraseña
|
// Validate new password
|
||||||
if dto.new_password.len() < 8 {
|
if dto.new_password.len() < 8 {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::InvalidInput,
|
ErrorKind::InvalidInput,
|
||||||
"User",
|
"User",
|
||||||
"Password debe tener al menos 8 caracteres"
|
"Password must be at least 8 characters long"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hashear nueva contraseña y actualizar usuario
|
// Hash new password and update user
|
||||||
let new_hash = self.password_hasher.hash_password(&dto.new_password)?;
|
let new_hash = self.password_hasher.hash_password(&dto.new_password)?;
|
||||||
user.update_password_hash(new_hash);
|
user.update_password_hash(new_hash);
|
||||||
|
|
||||||
// Guardar usuario actualizado
|
// Save updated user
|
||||||
self.user_storage.update_user(user).await?;
|
self.user_storage.update_user(user).await?;
|
||||||
|
|
||||||
// Opcional: revocar todas las sesiones para forzar re-login con nueva contraseña
|
// Optional: revoke all sessions to force re-login with new password
|
||||||
self.session_storage.revoke_all_user_sessions(user_id).await?;
|
self.session_storage.revoke_all_user_sessions(user_id).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -492,7 +492,7 @@ impl AuthApplicationService {
|
|||||||
.map_err(|e| DomainError::new(
|
.map_err(|e| DomainError::new(
|
||||||
ErrorKind::InternalError,
|
ErrorKind::InternalError,
|
||||||
"User",
|
"User",
|
||||||
format!("Error al contar usuarios administradores: {}", e)
|
format!("Error counting admin users: {}", e)
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
Ok(admin_users.len() as i64)
|
Ok(admin_users.len() as i64)
|
||||||
@@ -506,7 +506,7 @@ impl AuthApplicationService {
|
|||||||
.map_err(|e| DomainError::new(
|
.map_err(|e| DomainError::new(
|
||||||
ErrorKind::InternalError,
|
ErrorKind::InternalError,
|
||||||
"User",
|
"User",
|
||||||
format!("Error al contar usuarios: {}", e)
|
format!("Error counting users: {}", e)
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
Ok(all_users.len() as i64)
|
Ok(all_users.len() as i64)
|
||||||
@@ -523,7 +523,7 @@ impl AuthApplicationService {
|
|||||||
.map_err(|e| DomainError::new(
|
.map_err(|e| DomainError::new(
|
||||||
ErrorKind::InternalError,
|
ErrorKind::InternalError,
|
||||||
"User",
|
"User",
|
||||||
format!("Error al eliminar usuario admin predeterminado: {}", e)
|
format!("Error deleting default admin user: {}", e)
|
||||||
))
|
))
|
||||||
},
|
},
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -545,7 +545,7 @@ impl AuthApplicationService {
|
|||||||
.map_err(|e| DomainError::new(
|
.map_err(|e| DomainError::new(
|
||||||
ErrorKind::InternalError,
|
ErrorKind::InternalError,
|
||||||
"User",
|
"User",
|
||||||
format!("Error al eliminar usuario admin predeterminado: {}", e)
|
format!("Error deleting default admin user: {}", e)
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
// 3. Create new admin user with the provided credentials but admin role
|
// 3. Create new admin user with the provided credentials but admin role
|
||||||
@@ -564,7 +564,7 @@ impl AuthApplicationService {
|
|||||||
).map_err(|e| DomainError::new(
|
).map_err(|e| DomainError::new(
|
||||||
ErrorKind::InvalidInput,
|
ErrorKind::InvalidInput,
|
||||||
"User",
|
"User",
|
||||||
format!("Error al crear usuario admin: {}", e)
|
format!("Error creating admin user: {}", e)
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
// 4. Save the new admin user
|
// 4. Save the new admin user
|
||||||
@@ -580,7 +580,7 @@ impl AuthApplicationService {
|
|||||||
}).await {
|
}).await {
|
||||||
Ok(folder) => {
|
Ok(folder) => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Carpeta personal creada para el admin {}: {} (ID: {})",
|
"Personal folder created for admin {}: {} (ID: {})",
|
||||||
created_user.id(),
|
created_user.id(),
|
||||||
folder.name,
|
folder.name,
|
||||||
folder.id
|
folder.id
|
||||||
@@ -588,7 +588,7 @@ impl AuthApplicationService {
|
|||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"No se pudo crear la carpeta personal para el admin {}: {}",
|
"Could not create personal folder for admin {}: {}",
|
||||||
created_user.id(),
|
created_user.id(),
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
@@ -596,7 +596,7 @@ impl AuthApplicationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!("Admin personalizado creado: {}", created_user.id());
|
tracing::info!("Custom admin created: {}", created_user.id());
|
||||||
Ok(UserDto::from(created_user))
|
Ok(UserDto::from(created_user))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,52 +12,52 @@ use crate::application::ports::inbound::FolderUseCase;
|
|||||||
use crate::application::dtos::file_dto::FileDto;
|
use crate::application::dtos::file_dto::FileDto;
|
||||||
use crate::application::dtos::folder_dto::FolderDto;
|
use crate::application::dtos::folder_dto::FolderDto;
|
||||||
|
|
||||||
/// Errores específicos para operaciones por lotes
|
/// Specific errors for batch operations
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum BatchOperationError {
|
pub enum BatchOperationError {
|
||||||
#[error("Error de dominio: {0}")]
|
#[error("Domain error: {0}")]
|
||||||
Domain(#[from] DomainError),
|
Domain(#[from] DomainError),
|
||||||
|
|
||||||
#[error("Operación cancelada: {0}")]
|
#[error("Operation cancelled: {0}")]
|
||||||
Cancelled(String),
|
Cancelled(String),
|
||||||
|
|
||||||
#[error("Límite de concurrencia excedido: {0}")]
|
#[error("Concurrency limit exceeded: {0}")]
|
||||||
ConcurrencyLimit(String),
|
ConcurrencyLimit(String),
|
||||||
|
|
||||||
#[error("Error en operación del lote: {0} ({1} de {2} completadas)")]
|
#[error("Batch operation error: {0} ({1} of {2} completed)")]
|
||||||
PartialFailure(String, usize, usize),
|
PartialFailure(String, usize, usize),
|
||||||
|
|
||||||
#[error("Error interno: {0}")]
|
#[error("Internal error: {0}")]
|
||||||
Internal(String),
|
Internal(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resultado de una operación por lotes con estadísticas
|
/// Result of a batch operation with statistics
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct BatchResult<T> {
|
pub struct BatchResult<T> {
|
||||||
/// Resultados exitosos
|
/// Successful results
|
||||||
pub successful: Vec<T>,
|
pub successful: Vec<T>,
|
||||||
/// Operaciones fallidas con sus errores
|
/// Failed operations with their errors
|
||||||
pub failed: Vec<(String, String)>,
|
pub failed: Vec<(String, String)>,
|
||||||
/// Estadísticas de la operación
|
/// Operation statistics
|
||||||
pub stats: BatchStats,
|
pub stats: BatchStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Estadísticas de una operación por lotes
|
/// Statistics of a batch operation
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct BatchStats {
|
pub struct BatchStats {
|
||||||
/// Número total de operaciones
|
/// Total number of operations
|
||||||
pub total: usize,
|
pub total: usize,
|
||||||
/// Número de operaciones exitosas
|
/// Number of successful operations
|
||||||
pub successful: usize,
|
pub successful: usize,
|
||||||
/// Número de operaciones fallidas
|
/// Number of failed operations
|
||||||
pub failed: usize,
|
pub failed: usize,
|
||||||
/// Tiempo total de ejecución en milisegundos
|
/// Total execution time in milliseconds
|
||||||
pub execution_time_ms: u128,
|
pub execution_time_ms: u128,
|
||||||
/// Concurrencia máxima alcanzada
|
/// Maximum concurrency reached
|
||||||
pub max_concurrency: usize,
|
pub max_concurrency: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Servicio de operaciones por lotes
|
/// Batch operations service
|
||||||
pub struct BatchOperationService {
|
pub struct BatchOperationService {
|
||||||
file_retrieval: Arc<dyn FileRetrievalUseCase>,
|
file_retrieval: Arc<dyn FileRetrievalUseCase>,
|
||||||
file_management: Arc<dyn FileManagementUseCase>,
|
file_management: Arc<dyn FileManagementUseCase>,
|
||||||
@@ -67,14 +67,14 @@ pub struct BatchOperationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BatchOperationService {
|
impl BatchOperationService {
|
||||||
/// Crea una nueva instancia del servicio de operaciones por lotes
|
/// Creates a new instance of the batch operations service
|
||||||
pub fn new(
|
pub fn new(
|
||||||
file_retrieval: Arc<dyn FileRetrievalUseCase>,
|
file_retrieval: Arc<dyn FileRetrievalUseCase>,
|
||||||
file_management: Arc<dyn FileManagementUseCase>,
|
file_management: Arc<dyn FileManagementUseCase>,
|
||||||
folder_service: Arc<FolderService>,
|
folder_service: Arc<FolderService>,
|
||||||
config: AppConfig
|
config: AppConfig
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// Limitar la concurrencia basada en la configuración
|
// Limit concurrency based on configuration
|
||||||
let max_concurrency = config.concurrency.max_concurrent_files;
|
let max_concurrency = config.concurrency.max_concurrent_files;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -86,7 +86,7 @@ impl BatchOperationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una nueva instancia con la configuración por defecto
|
/// Creates a new instance with default configuration
|
||||||
pub fn default(
|
pub fn default(
|
||||||
file_retrieval: Arc<dyn FileRetrievalUseCase>,
|
file_retrieval: Arc<dyn FileRetrievalUseCase>,
|
||||||
file_management: Arc<dyn FileManagementUseCase>,
|
file_management: Arc<dyn FileManagementUseCase>,
|
||||||
@@ -95,16 +95,16 @@ impl BatchOperationService {
|
|||||||
Self::new(file_retrieval, file_management, folder_service, AppConfig::default())
|
Self::new(file_retrieval, file_management, folder_service, AppConfig::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copia múltiples archivos en paralelo
|
/// Copies multiple files in parallel
|
||||||
pub async fn copy_files(
|
pub async fn copy_files(
|
||||||
&self,
|
&self,
|
||||||
file_ids: Vec<String>,
|
file_ids: Vec<String>,
|
||||||
target_folder_id: Option<String>,
|
target_folder_id: Option<String>,
|
||||||
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
||||||
info!("Iniciando copia en lote de {} archivos", file_ids.len());
|
info!("Starting batch copy of {} files", file_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
// Crear estructura para el resultado
|
// Create result structure
|
||||||
let mut result = BatchResult {
|
let mut result = BatchResult {
|
||||||
successful: Vec::new(),
|
successful: Vec::new(),
|
||||||
failed: Vec::new(),
|
failed: Vec::new(),
|
||||||
@@ -114,30 +114,30 @@ impl BatchOperationService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Definir la operación a realizar para cada archivo
|
// Define the operation to perform for each file
|
||||||
let operations = file_ids.into_iter().map(|file_id| {
|
let operations = file_ids.into_iter().map(|file_id| {
|
||||||
let mgmt = self.file_management.clone();
|
let mgmt = self.file_management.clone();
|
||||||
let target_folder = target_folder_id.clone();
|
let target_folder = target_folder_id.clone();
|
||||||
let semaphore = self.semaphore.clone();
|
let semaphore = self.semaphore.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// Adquirir permiso del semáforo
|
// Acquire semaphore permit
|
||||||
let permit = semaphore.acquire().await.unwrap();
|
let permit = semaphore.acquire().await.unwrap();
|
||||||
|
|
||||||
let copy_result = mgmt.move_file(&file_id, target_folder.clone()).await;
|
let copy_result = mgmt.move_file(&file_id, target_folder.clone()).await;
|
||||||
|
|
||||||
// Liberar el permiso explícitamente (también se libera al hacer drop)
|
// Release the permit explicitly (also released on drop)
|
||||||
drop(permit);
|
drop(permit);
|
||||||
|
|
||||||
// Devolver el resultado junto con el ID para identificar éxitos/fallos
|
// Return the result along with the ID to identify successes/failures
|
||||||
(file_id, copy_result)
|
(file_id, copy_result)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ejecutar todas las operaciones en paralelo con control de concurrencia
|
// Execute all operations in parallel with concurrency control
|
||||||
let operation_results = join_all(operations).await;
|
let operation_results = join_all(operations).await;
|
||||||
|
|
||||||
// Procesar los resultados
|
// Process the results
|
||||||
for (file_id, operation_result) in operation_results {
|
for (file_id, operation_result) in operation_results {
|
||||||
match operation_result {
|
match operation_result {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
@@ -151,13 +151,13 @@ impl BatchOperationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completar estadísticas
|
// Complete statistics
|
||||||
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
||||||
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
||||||
.min(result.stats.total);
|
.min(result.stats.total);
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Copia en lote completada: {}/{} exitosas en {}ms",
|
"Batch copy completed: {}/{} successful in {}ms",
|
||||||
result.stats.successful,
|
result.stats.successful,
|
||||||
result.stats.total,
|
result.stats.total,
|
||||||
result.stats.execution_time_ms
|
result.stats.execution_time_ms
|
||||||
@@ -166,16 +166,16 @@ impl BatchOperationService {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mueve múltiples archivos en paralelo
|
/// Moves multiple files in parallel
|
||||||
pub async fn move_files(
|
pub async fn move_files(
|
||||||
&self,
|
&self,
|
||||||
file_ids: Vec<String>,
|
file_ids: Vec<String>,
|
||||||
target_folder_id: Option<String>,
|
target_folder_id: Option<String>,
|
||||||
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
||||||
info!("Iniciando movimiento en lote de {} archivos", file_ids.len());
|
info!("Starting batch move of {} files", file_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
// Crear estructura para el resultado
|
// Create result structure
|
||||||
let mut result = BatchResult {
|
let mut result = BatchResult {
|
||||||
successful: Vec::new(),
|
successful: Vec::new(),
|
||||||
failed: Vec::new(),
|
failed: Vec::new(),
|
||||||
@@ -185,30 +185,30 @@ impl BatchOperationService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Definir la operación a realizar para cada archivo
|
// Define the operation to perform for each file
|
||||||
let operations = file_ids.into_iter().map(|file_id| {
|
let operations = file_ids.into_iter().map(|file_id| {
|
||||||
let mgmt = self.file_management.clone();
|
let mgmt = self.file_management.clone();
|
||||||
let target_folder = target_folder_id.clone();
|
let target_folder = target_folder_id.clone();
|
||||||
let semaphore = self.semaphore.clone();
|
let semaphore = self.semaphore.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// Adquirir permiso del semáforo
|
// Acquire semaphore permit
|
||||||
let permit = semaphore.acquire().await.unwrap();
|
let permit = semaphore.acquire().await.unwrap();
|
||||||
|
|
||||||
let move_result = mgmt.move_file(&file_id, target_folder.clone()).await;
|
let move_result = mgmt.move_file(&file_id, target_folder.clone()).await;
|
||||||
|
|
||||||
// Liberar el permiso explícitamente
|
// Release the permit explicitly
|
||||||
drop(permit);
|
drop(permit);
|
||||||
|
|
||||||
// Devolver el resultado junto con el ID para identificar éxitos/fallos
|
// Return the result along with the ID to identify successes/failures
|
||||||
(file_id, move_result)
|
(file_id, move_result)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ejecutar todas las operaciones en paralelo con control de concurrencia
|
// Execute all operations in parallel with concurrency control
|
||||||
let operation_results = join_all(operations).await;
|
let operation_results = join_all(operations).await;
|
||||||
|
|
||||||
// Procesar los resultados
|
// Process the results
|
||||||
for (file_id, operation_result) in operation_results {
|
for (file_id, operation_result) in operation_results {
|
||||||
match operation_result {
|
match operation_result {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
@@ -222,13 +222,13 @@ impl BatchOperationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completar estadísticas
|
// Complete statistics
|
||||||
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
||||||
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
||||||
.min(result.stats.total);
|
.min(result.stats.total);
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Movimiento en lote completado: {}/{} exitosas en {}ms",
|
"Batch move completed: {}/{} successful in {}ms",
|
||||||
result.stats.successful,
|
result.stats.successful,
|
||||||
result.stats.total,
|
result.stats.total,
|
||||||
result.stats.execution_time_ms
|
result.stats.execution_time_ms
|
||||||
@@ -237,15 +237,15 @@ impl BatchOperationService {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina múltiples archivos en paralelo
|
/// Deletes multiple files in parallel
|
||||||
pub async fn delete_files(
|
pub async fn delete_files(
|
||||||
&self,
|
&self,
|
||||||
file_ids: Vec<String>,
|
file_ids: Vec<String>,
|
||||||
) -> Result<BatchResult<String>, BatchOperationError> {
|
) -> Result<BatchResult<String>, BatchOperationError> {
|
||||||
info!("Iniciando eliminación en lote de {} archivos", file_ids.len());
|
info!("Starting batch deletion of {} files", file_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
// Crear estructura para el resultado
|
// Create result structure
|
||||||
let mut result = BatchResult {
|
let mut result = BatchResult {
|
||||||
successful: Vec::new(),
|
successful: Vec::new(),
|
||||||
failed: Vec::new(),
|
failed: Vec::new(),
|
||||||
@@ -255,30 +255,30 @@ impl BatchOperationService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Definir la operación a realizar para cada archivo
|
// Define the operation to perform for each file
|
||||||
let operations = file_ids.into_iter().map(|file_id| {
|
let operations = file_ids.into_iter().map(|file_id| {
|
||||||
let mgmt = self.file_management.clone();
|
let mgmt = self.file_management.clone();
|
||||||
let semaphore = self.semaphore.clone();
|
let semaphore = self.semaphore.clone();
|
||||||
let id_clone = file_id.clone();
|
let id_clone = file_id.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// Adquirir permiso del semáforo
|
// Acquire semaphore permit
|
||||||
let permit = semaphore.acquire().await.unwrap();
|
let permit = semaphore.acquire().await.unwrap();
|
||||||
|
|
||||||
let delete_result = mgmt.delete_file(&file_id).await;
|
let delete_result = mgmt.delete_file(&file_id).await;
|
||||||
|
|
||||||
// Liberar el permiso explícitamente
|
// Release the permit explicitly
|
||||||
drop(permit);
|
drop(permit);
|
||||||
|
|
||||||
// Devolver el resultado junto con el ID
|
// Return the result along with the ID
|
||||||
(id_clone.clone(), delete_result.map(|_| id_clone))
|
(id_clone.clone(), delete_result.map(|_| id_clone))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ejecutar todas las operaciones en paralelo con control de concurrencia
|
// Execute all operations in parallel with concurrency control
|
||||||
let operation_results = join_all(operations).await;
|
let operation_results = join_all(operations).await;
|
||||||
|
|
||||||
// Procesar los resultados
|
// Process the results
|
||||||
for (file_id, operation_result) in operation_results {
|
for (file_id, operation_result) in operation_results {
|
||||||
match operation_result {
|
match operation_result {
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
@@ -292,13 +292,13 @@ impl BatchOperationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completar estadísticas
|
// Complete statistics
|
||||||
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
||||||
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
||||||
.min(result.stats.total);
|
.min(result.stats.total);
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Eliminación en lote completada: {}/{} exitosas en {}ms",
|
"Batch deletion completed: {}/{} successful in {}ms",
|
||||||
result.stats.successful,
|
result.stats.successful,
|
||||||
result.stats.total,
|
result.stats.total,
|
||||||
result.stats.execution_time_ms
|
result.stats.execution_time_ms
|
||||||
@@ -307,15 +307,15 @@ impl BatchOperationService {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Carga múltiples archivos en paralelo (datos en memoria)
|
/// Loads multiple files in parallel (data in memory)
|
||||||
pub async fn get_multiple_files(
|
pub async fn get_multiple_files(
|
||||||
&self,
|
&self,
|
||||||
file_ids: Vec<String>,
|
file_ids: Vec<String>,
|
||||||
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
||||||
info!("Iniciando carga en lote de {} archivos", file_ids.len());
|
info!("Starting batch load of {} files", file_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
// Crear estructura para el resultado
|
// Create result structure
|
||||||
let mut result = BatchResult {
|
let mut result = BatchResult {
|
||||||
successful: Vec::new(),
|
successful: Vec::new(),
|
||||||
failed: Vec::new(),
|
failed: Vec::new(),
|
||||||
@@ -325,29 +325,29 @@ impl BatchOperationService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Definir la operación a realizar para cada archivo
|
// Define the operation to perform for each file
|
||||||
let operations = file_ids.into_iter().map(|file_id| {
|
let operations = file_ids.into_iter().map(|file_id| {
|
||||||
let retrieval = self.file_retrieval.clone();
|
let retrieval = self.file_retrieval.clone();
|
||||||
let semaphore = self.semaphore.clone();
|
let semaphore = self.semaphore.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// Adquirir permiso del semáforo
|
// Acquire semaphore permit
|
||||||
let permit = semaphore.acquire().await.unwrap();
|
let permit = semaphore.acquire().await.unwrap();
|
||||||
|
|
||||||
let get_result = retrieval.get_file(&file_id).await;
|
let get_result = retrieval.get_file(&file_id).await;
|
||||||
|
|
||||||
// Liberar el permiso explícitamente
|
// Release the permit explicitly
|
||||||
drop(permit);
|
drop(permit);
|
||||||
|
|
||||||
// Devolver el resultado junto con el ID
|
// Return the result along with the ID
|
||||||
(file_id, get_result)
|
(file_id, get_result)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ejecutar todas las operaciones en paralelo con control de concurrencia
|
// Execute all operations in parallel with concurrency control
|
||||||
let operation_results = join_all(operations).await;
|
let operation_results = join_all(operations).await;
|
||||||
|
|
||||||
// Procesar los resultados
|
// Process the results
|
||||||
for (file_id, operation_result) in operation_results {
|
for (file_id, operation_result) in operation_results {
|
||||||
match operation_result {
|
match operation_result {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
@@ -361,13 +361,13 @@ impl BatchOperationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completar estadísticas
|
// Complete statistics
|
||||||
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
||||||
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
||||||
.min(result.stats.total);
|
.min(result.stats.total);
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Carga en lote completada: {}/{} exitosas en {}ms",
|
"Batch load completed: {}/{} successful in {}ms",
|
||||||
result.stats.successful,
|
result.stats.successful,
|
||||||
result.stats.total,
|
result.stats.total,
|
||||||
result.stats.execution_time_ms
|
result.stats.execution_time_ms
|
||||||
@@ -376,16 +376,16 @@ impl BatchOperationService {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina múltiples carpetas en paralelo
|
/// Deletes multiple folders in parallel
|
||||||
pub async fn delete_folders(
|
pub async fn delete_folders(
|
||||||
&self,
|
&self,
|
||||||
folder_ids: Vec<String>,
|
folder_ids: Vec<String>,
|
||||||
_recursive: bool,
|
_recursive: bool,
|
||||||
) -> Result<BatchResult<String>, BatchOperationError> {
|
) -> Result<BatchResult<String>, BatchOperationError> {
|
||||||
info!("Iniciando eliminación en lote de {} carpetas", folder_ids.len());
|
info!("Starting batch deletion of {} folders", folder_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
// Crear estructura para el resultado
|
// Create result structure
|
||||||
let mut result = BatchResult {
|
let mut result = BatchResult {
|
||||||
successful: Vec::new(),
|
successful: Vec::new(),
|
||||||
failed: Vec::new(),
|
failed: Vec::new(),
|
||||||
@@ -395,32 +395,32 @@ impl BatchOperationService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Definir la operación a realizar para cada carpeta
|
// Define the operation to perform for each folder
|
||||||
let operations = folder_ids.into_iter().map(|folder_id| {
|
let operations = folder_ids.into_iter().map(|folder_id| {
|
||||||
let folder_service = self.folder_service.clone();
|
let folder_service = self.folder_service.clone();
|
||||||
let semaphore = self.semaphore.clone();
|
let semaphore = self.semaphore.clone();
|
||||||
let id_clone = folder_id.clone();
|
let id_clone = folder_id.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// Adquirir permiso del semáforo
|
// Acquire semaphore permit
|
||||||
let permit = semaphore.acquire().await.unwrap();
|
let permit = semaphore.acquire().await.unwrap();
|
||||||
|
|
||||||
// For both recursive and non-recursive, use the standard delete_folder method
|
// For both recursive and non-recursive, use the standard delete_folder method
|
||||||
// since FolderUseCase only has a single delete_folder method
|
// since FolderUseCase only has a single delete_folder method
|
||||||
let delete_result = folder_service.delete_folder(&folder_id).await;
|
let delete_result = folder_service.delete_folder(&folder_id).await;
|
||||||
|
|
||||||
// Liberar el permiso explícitamente
|
// Release the permit explicitly
|
||||||
drop(permit);
|
drop(permit);
|
||||||
|
|
||||||
// Devolver el resultado junto con el ID
|
// Return the result along with the ID
|
||||||
(id_clone.clone(), delete_result.map(|_| id_clone))
|
(id_clone.clone(), delete_result.map(|_| id_clone))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ejecutar todas las operaciones en paralelo con control de concurrencia
|
// Execute all operations in parallel with concurrency control
|
||||||
let operation_results = join_all(operations).await;
|
let operation_results = join_all(operations).await;
|
||||||
|
|
||||||
// Procesar los resultados
|
// Process the results
|
||||||
for (folder_id, operation_result) in operation_results {
|
for (folder_id, operation_result) in operation_results {
|
||||||
match operation_result {
|
match operation_result {
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
@@ -434,13 +434,13 @@ impl BatchOperationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completar estadísticas
|
// Complete statistics
|
||||||
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
||||||
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
||||||
.min(result.stats.total);
|
.min(result.stats.total);
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Eliminación en lote de carpetas completada: {}/{} exitosas en {}ms",
|
"Batch folder deletion completed: {}/{} successful in {}ms",
|
||||||
result.stats.successful,
|
result.stats.successful,
|
||||||
result.stats.total,
|
result.stats.total,
|
||||||
result.stats.execution_time_ms
|
result.stats.execution_time_ms
|
||||||
@@ -449,7 +449,7 @@ impl BatchOperationService {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Operación genérica de lote para cualquier tipo de función asíncrona
|
/// Generic batch operation for any type of async function
|
||||||
pub async fn generic_batch_operation<T, F, Fut>(
|
pub async fn generic_batch_operation<T, F, Fut>(
|
||||||
&self,
|
&self,
|
||||||
items: Vec<T>,
|
items: Vec<T>,
|
||||||
@@ -460,10 +460,10 @@ impl BatchOperationService {
|
|||||||
F: Fn(T, Arc<Semaphore>) -> Fut + Clone + Send + Sync + 'static,
|
F: Fn(T, Arc<Semaphore>) -> Fut + Clone + Send + Sync + 'static,
|
||||||
Fut: Future<Output = Result<T, DomainError>> + Send + 'static,
|
Fut: Future<Output = Result<T, DomainError>> + Send + 'static,
|
||||||
{
|
{
|
||||||
info!("Iniciando operación genérica en lote con {} items", items.len());
|
info!("Starting generic batch operation with {} items", items.len());
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
// Crear estructura para el resultado
|
// Create result structure
|
||||||
let mut result = BatchResult {
|
let mut result = BatchResult {
|
||||||
successful: Vec::new(),
|
successful: Vec::new(),
|
||||||
failed: Vec::new(),
|
failed: Vec::new(),
|
||||||
@@ -473,25 +473,25 @@ impl BatchOperationService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convertir cada item a una tarea
|
// Convert each item to a task
|
||||||
let tasks = items.iter().map(|item| {
|
let tasks = items.iter().map(|item| {
|
||||||
let item_clone = item.clone();
|
let item_clone = item.clone();
|
||||||
let op = operation.clone();
|
let op = operation.clone();
|
||||||
let semaphore = self.semaphore.clone();
|
let semaphore = self.semaphore.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// La función proporcionada debe manejar la adquisición del semáforo
|
// The provided function must handle semaphore acquisition
|
||||||
let op_result = op(item_clone.clone(), semaphore).await;
|
let op_result = op(item_clone.clone(), semaphore).await;
|
||||||
|
|
||||||
// Devolver el resultado junto con el item original para identificación
|
// Return the result along with the original item for identification
|
||||||
(item_clone, op_result)
|
(item_clone, op_result)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ejecutar todas las tareas en paralelo
|
// Execute all tasks in parallel
|
||||||
let operation_results = join_all(tasks).await;
|
let operation_results = join_all(tasks).await;
|
||||||
|
|
||||||
// Procesar resultados
|
// Process results
|
||||||
for (item, operation_result) in operation_results {
|
for (item, operation_result) in operation_results {
|
||||||
match operation_result {
|
match operation_result {
|
||||||
Ok(result_item) => {
|
Ok(result_item) => {
|
||||||
@@ -499,20 +499,20 @@ impl BatchOperationService {
|
|||||||
result.stats.successful += 1;
|
result.stats.successful += 1;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Convertir el item a string para el reporte de error
|
// Convert item to string for error reporting
|
||||||
result.failed.push((format!("{:?}", item), e.to_string()));
|
result.failed.push((format!("{:?}", item), e.to_string()));
|
||||||
result.stats.failed += 1;
|
result.stats.failed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completar estadísticas
|
// Complete statistics
|
||||||
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
||||||
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
||||||
.min(result.stats.total);
|
.min(result.stats.total);
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Operación genérica en lote completada: {}/{} exitosas en {}ms",
|
"Generic batch operation completed: {}/{} successful in {}ms",
|
||||||
result.stats.successful,
|
result.stats.successful,
|
||||||
result.stats.total,
|
result.stats.total,
|
||||||
result.stats.execution_time_ms
|
result.stats.execution_time_ms
|
||||||
@@ -521,15 +521,15 @@ impl BatchOperationService {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crear múltiples carpetas en paralelo
|
/// Create multiple folders in parallel
|
||||||
pub async fn create_folders(
|
pub async fn create_folders(
|
||||||
&self,
|
&self,
|
||||||
folders: Vec<(String, Option<String>)>, // (nombre, padre_id)
|
folders: Vec<(String, Option<String>)>, // (name, parent_id)
|
||||||
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
||||||
info!("Iniciando creación en lote de {} carpetas", folders.len());
|
info!("Starting batch creation of {} folders", folders.len());
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
// Crear estructura para el resultado
|
// Create result structure
|
||||||
let mut result = BatchResult {
|
let mut result = BatchResult {
|
||||||
successful: Vec::new(),
|
successful: Vec::new(),
|
||||||
failed: Vec::new(),
|
failed: Vec::new(),
|
||||||
@@ -539,13 +539,13 @@ impl BatchOperationService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Definir la operación para cada carpeta
|
// Define the operation for each folder
|
||||||
let operations = folders.into_iter().map(|(name, parent_id)| {
|
let operations = folders.into_iter().map(|(name, parent_id)| {
|
||||||
let folder_service = self.folder_service.clone();
|
let folder_service = self.folder_service.clone();
|
||||||
let semaphore = self.semaphore.clone();
|
let semaphore = self.semaphore.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// Adquirir permiso del semáforo
|
// Acquire semaphore permit
|
||||||
let permit = semaphore.acquire().await.unwrap();
|
let permit = semaphore.acquire().await.unwrap();
|
||||||
|
|
||||||
let dto = crate::application::dtos::folder_dto::CreateFolderDto {
|
let dto = crate::application::dtos::folder_dto::CreateFolderDto {
|
||||||
@@ -554,19 +554,19 @@ impl BatchOperationService {
|
|||||||
};
|
};
|
||||||
let create_result = folder_service.create_folder(dto).await;
|
let create_result = folder_service.create_folder(dto).await;
|
||||||
|
|
||||||
// Liberar el permiso explícitamente
|
// Release the permit explicitly
|
||||||
drop(permit);
|
drop(permit);
|
||||||
|
|
||||||
// Devolver el resultado con un identificador para los errores
|
// Return the result with an identifier for errors
|
||||||
let id = format!("{}:{}", name, parent_id.unwrap_or_default());
|
let id = format!("{}:{}", name, parent_id.unwrap_or_default());
|
||||||
(id, create_result)
|
(id, create_result)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ejecutar todas las operaciones en paralelo
|
// Execute all operations in parallel
|
||||||
let operation_results = join_all(operations).await;
|
let operation_results = join_all(operations).await;
|
||||||
|
|
||||||
// Procesar los resultados
|
// Process the results
|
||||||
for (id, operation_result) in operation_results {
|
for (id, operation_result) in operation_results {
|
||||||
match operation_result {
|
match operation_result {
|
||||||
Ok(folder) => {
|
Ok(folder) => {
|
||||||
@@ -580,13 +580,13 @@ impl BatchOperationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completar estadísticas
|
// Complete statistics
|
||||||
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
||||||
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
||||||
.min(result.stats.total);
|
.min(result.stats.total);
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Creación en lote de carpetas completada: {}/{} exitosas en {}ms",
|
"Batch folder creation completed: {}/{} successful in {}ms",
|
||||||
result.stats.successful,
|
result.stats.successful,
|
||||||
result.stats.total,
|
result.stats.total,
|
||||||
result.stats.execution_time_ms
|
result.stats.execution_time_ms
|
||||||
@@ -595,15 +595,15 @@ impl BatchOperationService {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtener metadatos de múltiples carpetas en paralelo
|
/// Get metadata of multiple folders in parallel
|
||||||
pub async fn get_multiple_folders(
|
pub async fn get_multiple_folders(
|
||||||
&self,
|
&self,
|
||||||
folder_ids: Vec<String>,
|
folder_ids: Vec<String>,
|
||||||
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
||||||
info!("Iniciando carga en lote de {} carpetas", folder_ids.len());
|
info!("Starting batch load of {} folders", folder_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
// Crear estructura para el resultado
|
// Create result structure
|
||||||
let mut result = BatchResult {
|
let mut result = BatchResult {
|
||||||
successful: Vec::new(),
|
successful: Vec::new(),
|
||||||
failed: Vec::new(),
|
failed: Vec::new(),
|
||||||
@@ -613,29 +613,29 @@ impl BatchOperationService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Definir la operación para cada carpeta
|
// Define the operation for each folder
|
||||||
let operations = folder_ids.into_iter().map(|folder_id| {
|
let operations = folder_ids.into_iter().map(|folder_id| {
|
||||||
let folder_service = self.folder_service.clone();
|
let folder_service = self.folder_service.clone();
|
||||||
let semaphore = self.semaphore.clone();
|
let semaphore = self.semaphore.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// Adquirir permiso del semáforo
|
// Acquire semaphore permit
|
||||||
let permit = semaphore.acquire().await.unwrap();
|
let permit = semaphore.acquire().await.unwrap();
|
||||||
|
|
||||||
let get_result = folder_service.get_folder(&folder_id).await;
|
let get_result = folder_service.get_folder(&folder_id).await;
|
||||||
|
|
||||||
// Liberar el permiso explícitamente
|
// Release the permit explicitly
|
||||||
drop(permit);
|
drop(permit);
|
||||||
|
|
||||||
// Devolver el resultado con su ID
|
// Return the result with its ID
|
||||||
(folder_id, get_result)
|
(folder_id, get_result)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ejecutar todas las operaciones en paralelo
|
// Execute all operations in parallel
|
||||||
let operation_results = join_all(operations).await;
|
let operation_results = join_all(operations).await;
|
||||||
|
|
||||||
// Procesar los resultados
|
// Process the results
|
||||||
for (folder_id, operation_result) in operation_results {
|
for (folder_id, operation_result) in operation_results {
|
||||||
match operation_result {
|
match operation_result {
|
||||||
Ok(folder) => {
|
Ok(folder) => {
|
||||||
@@ -649,13 +649,13 @@ impl BatchOperationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completar estadísticas
|
// Complete statistics
|
||||||
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
result.stats.execution_time_ms = start_time.elapsed().as_millis();
|
||||||
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files
|
||||||
.min(result.stats.total);
|
.min(result.stats.total);
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Carga en lote de carpetas completada: {}/{} exitosas en {}ms",
|
"Batch folder load completed: {}/{} successful in {}ms",
|
||||||
result.stats.successful,
|
result.stats.successful,
|
||||||
result.stats.total,
|
result.stats.total,
|
||||||
result.stats.execution_time_ms
|
result.stats.execution_time_ms
|
||||||
@@ -673,7 +673,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_generic_batch_operation() {
|
async fn test_generic_batch_operation() {
|
||||||
// Crear el servicio de batch with stubs
|
// Create the batch service with stubs
|
||||||
let batch_service = BatchOperationService::new(
|
let batch_service = BatchOperationService::new(
|
||||||
Arc::new(StubFileRetrievalUseCase),
|
Arc::new(StubFileRetrievalUseCase),
|
||||||
Arc::new(StubFileManagementUseCase),
|
Arc::new(StubFileManagementUseCase),
|
||||||
@@ -683,35 +683,35 @@ mod tests {
|
|||||||
AppConfig::default()
|
AppConfig::default()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Definir una operación genérica de prueba
|
// Define a generic test operation
|
||||||
let operation = |item: i32, semaphore: Arc<Semaphore>| async move {
|
let operation = |item: i32, semaphore: Arc<Semaphore>| async move {
|
||||||
// Adquirir y liberar el semáforo
|
// Acquire and release the semaphore
|
||||||
let _permit = semaphore.acquire().await.unwrap();
|
let _permit = semaphore.acquire().await.unwrap();
|
||||||
|
|
||||||
if item % 2 == 0 {
|
if item % 2 == 0 {
|
||||||
// Simular éxito para números pares
|
// Simulate success for even numbers
|
||||||
Ok(item * 2)
|
Ok(item * 2)
|
||||||
} else {
|
} else {
|
||||||
// Simular error para números impares
|
// Simulate error for odd numbers
|
||||||
Err(DomainError::validation_error("Odd number not allowed"))
|
Err(DomainError::validation_error("Odd number not allowed"))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ejecutar la operación de batch
|
// Execute the batch operation
|
||||||
let items = vec![1, 2, 3, 4, 5];
|
let items = vec![1, 2, 3, 4, 5];
|
||||||
|
|
||||||
let result = batch_service.generic_batch_operation(items, operation).await.unwrap();
|
let result = batch_service.generic_batch_operation(items, operation).await.unwrap();
|
||||||
|
|
||||||
// Verificar los resultados
|
// Verify the results
|
||||||
assert_eq!(result.stats.total, 5);
|
assert_eq!(result.stats.total, 5);
|
||||||
assert_eq!(result.stats.successful, 2);
|
assert_eq!(result.stats.successful, 2);
|
||||||
assert_eq!(result.stats.failed, 3);
|
assert_eq!(result.stats.failed, 3);
|
||||||
|
|
||||||
// Los números pares deberían estar en los éxitos, duplicados
|
// Even numbers should be in successes, doubled
|
||||||
assert!(result.successful.contains(&4)); // 2*2
|
assert!(result.successful.contains(&4)); // 2*2
|
||||||
assert!(result.successful.contains(&8)); // 4*2
|
assert!(result.successful.contains(&8)); // 4*2
|
||||||
|
|
||||||
// Los impares deberían estar en los fallos
|
// Odd numbers should be in failures
|
||||||
assert_eq!(result.failed.len(), 3);
|
assert_eq!(result.failed.len(), 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ const CACHE_THRESHOLD: u64 = 10 * 1024 * 1024;
|
|||||||
/// Threshold above which mmap is used instead of streaming (100 MB).
|
/// Threshold above which mmap is used instead of streaming (100 MB).
|
||||||
const MMAP_THRESHOLD: u64 = 100 * 1024 * 1024;
|
const MMAP_THRESHOLD: u64 = 100 * 1024 * 1024;
|
||||||
|
|
||||||
/// Servicio para operaciones de recuperación de archivos
|
/// Service for file retrieval operations
|
||||||
///
|
///
|
||||||
/// Implements a multi-tier download strategy:
|
/// Implements a multi-tier download strategy:
|
||||||
/// - Tier 0: Write-behind cache (just-uploaded files still in RAM)
|
/// - Tier 0: Write-behind cache (just-uploaded files still in RAM)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ fn extract_username_from_path(path: &str) -> Option<String> {
|
|||||||
Some(parts[1].trim().to_string())
|
Some(parts[1].trim().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Servicio para operaciones de subida de archivos
|
/// Service for file upload operations
|
||||||
///
|
///
|
||||||
/// Encapsulates the three-tier upload strategy:
|
/// Encapsulates the three-tier upload strategy:
|
||||||
/// 1. **Write-Behind** (<256 KB): store in RAM, respond instantly, flush async.
|
/// 1. **Write-Behind** (<256 KB): store in RAM, respond instantly, flush async.
|
||||||
@@ -78,7 +78,7 @@ impl FileUploadService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configura el servicio de uso de almacenamiento
|
/// Configures the storage usage service
|
||||||
pub fn with_storage_usage_service(
|
pub fn with_storage_usage_service(
|
||||||
mut self,
|
mut self,
|
||||||
storage_usage_service: Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>,
|
storage_usage_service: Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>,
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ use crate::application::services::file_retrieval_service::FileRetrievalService;
|
|||||||
use crate::application::services::file_management_service::FileManagementService;
|
use crate::application::services::file_management_service::FileManagementService;
|
||||||
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
|
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
|
||||||
|
|
||||||
/// Factory para crear implementaciones de casos de uso de archivos
|
/// Factory for creating file use case implementations
|
||||||
pub struct AppFileUseCaseFactory {
|
pub struct AppFileUseCaseFactory {
|
||||||
file_read_repository: Arc<dyn FileReadPort>,
|
file_read_repository: Arc<dyn FileReadPort>,
|
||||||
file_write_repository: Arc<dyn FileWritePort>,
|
file_write_repository: Arc<dyn FileWritePort>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppFileUseCaseFactory {
|
impl AppFileUseCaseFactory {
|
||||||
/// Crea una nueva factory para casos de uso de archivos
|
/// Creates a new factory for file use cases
|
||||||
pub fn new(
|
pub fn new(
|
||||||
file_read_repository: Arc<dyn FileReadPort>,
|
file_read_repository: Arc<dyn FileReadPort>,
|
||||||
file_write_repository: Arc<dyn FileWritePort>
|
file_write_repository: Arc<dyn FileWritePort>
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ use crate::application::ports::outbound::FolderStoragePort;
|
|||||||
use crate::application::transactions::storage_transaction::StorageTransaction;
|
use crate::application::transactions::storage_transaction::StorageTransaction;
|
||||||
use crate::common::errors::{DomainError, ErrorKind};
|
use crate::common::errors::{DomainError, ErrorKind};
|
||||||
|
|
||||||
/// Implementación del caso de uso para operaciones de carpetas
|
/// Implementation of the use case for folder operations
|
||||||
pub struct FolderService {
|
pub struct FolderService {
|
||||||
folder_storage: Arc<dyn FolderStoragePort>,
|
folder_storage: Arc<dyn FolderStoragePort>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FolderService {
|
impl FolderService {
|
||||||
/// Crea un nuevo servicio de carpetas
|
/// Creates a new folder service
|
||||||
pub fn new(folder_storage: Arc<dyn FolderStoragePort>) -> Self {
|
pub fn new(folder_storage: Arc<dyn FolderStoragePort>) -> Self {
|
||||||
Self { folder_storage }
|
Self { folder_storage }
|
||||||
}
|
}
|
||||||
@@ -72,9 +72,9 @@ impl FolderService {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl FolderUseCase for FolderService {
|
impl FolderUseCase for FolderService {
|
||||||
/// Crea una nueva carpeta
|
/// Creates a new folder
|
||||||
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
|
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
|
||||||
// Validación de entrada
|
// Input validation
|
||||||
if dto.name.is_empty() {
|
if dto.name.is_empty() {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::InvalidInput,
|
ErrorKind::InvalidInput,
|
||||||
@@ -83,7 +83,7 @@ impl FolderUseCase for FolderService {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si se proporciona un parent_id, verificar que existe
|
// If a parent_id is provided, verify it exists
|
||||||
if let Some(parent_id) = &dto.parent_id {
|
if let Some(parent_id) = &dto.parent_id {
|
||||||
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
|
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
|
||||||
if !parent_exists {
|
if !parent_exists {
|
||||||
@@ -91,16 +91,16 @@ impl FolderUseCase for FolderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crear la carpeta
|
// Create the folder
|
||||||
let folder = self.folder_storage.create_folder(dto.name, dto.parent_id)
|
let folder = self.folder_storage.create_folder(dto.name, dto.parent_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to create folder: {}", e)))?;
|
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to create folder: {}", e)))?;
|
||||||
|
|
||||||
// Convertir a DTO
|
// Convert to DTO
|
||||||
Ok(FolderDto::from(folder))
|
Ok(FolderDto::from(folder))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene una carpeta por su ID
|
/// Gets a folder by its ID
|
||||||
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
|
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
|
||||||
let folder = self.folder_storage.get_folder(id)
|
let folder = self.folder_storage.get_folder(id)
|
||||||
.await
|
.await
|
||||||
@@ -109,9 +109,9 @@ impl FolderUseCase for FolderService {
|
|||||||
Ok(FolderDto::from(folder))
|
Ok(FolderDto::from(folder))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene una carpeta por su ruta
|
/// Gets a folder by its path
|
||||||
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError> {
|
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError> {
|
||||||
// Convertir la ruta de string a StoragePath
|
// Convert the string path to StoragePath
|
||||||
let storage_path = StoragePath::from_string(path);
|
let storage_path = StoragePath::from_string(path);
|
||||||
|
|
||||||
let folder = self.folder_storage.get_folder_by_path(&storage_path)
|
let folder = self.folder_storage.get_folder_by_path(&storage_path)
|
||||||
@@ -121,39 +121,39 @@ impl FolderUseCase for FolderService {
|
|||||||
Ok(FolderDto::from(folder))
|
Ok(FolderDto::from(folder))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lista carpetas dentro de una carpeta padre
|
/// Lists folders within a parent folder
|
||||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> {
|
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> {
|
||||||
let folders = self.folder_storage.list_folders(parent_id)
|
let folders = self.folder_storage.list_folders(parent_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders in parent: {:?}: {}", parent_id, e)))?;
|
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders in parent: {:?}: {}", parent_id, e)))?;
|
||||||
|
|
||||||
// Convertir a DTOs
|
// Convert to DTOs
|
||||||
Ok(folders.into_iter().map(FolderDto::from).collect())
|
Ok(folders.into_iter().map(FolderDto::from).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lista carpetas con paginación
|
/// Lists folders with pagination
|
||||||
async fn list_folders_paginated(
|
async fn list_folders_paginated(
|
||||||
&self,
|
&self,
|
||||||
parent_id: Option<&str>,
|
parent_id: Option<&str>,
|
||||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto
|
pagination: &crate::application::dtos::pagination::PaginationRequestDto
|
||||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError> {
|
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError> {
|
||||||
// Validar y ajustar la paginación
|
// Validate and adjust pagination
|
||||||
let pagination = pagination.validate_and_adjust();
|
let pagination = pagination.validate_and_adjust();
|
||||||
|
|
||||||
// Obtener carpetas paginadas y conteo total
|
// Get paginated folders and total count
|
||||||
let (folders, total_items) = self.folder_storage.list_folders_paginated(
|
let (folders, total_items) = self.folder_storage.list_folders_paginated(
|
||||||
parent_id,
|
parent_id,
|
||||||
pagination.offset(),
|
pagination.offset(),
|
||||||
pagination.limit(),
|
pagination.limit(),
|
||||||
true // Siempre incluir total para mejor UX
|
true // Always include total for better UX
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders with pagination in parent: {:?}: {}", parent_id, e)))?;
|
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders with pagination in parent: {:?}: {}", parent_id, e)))?;
|
||||||
|
|
||||||
// El total es necesario para calcular la paginación
|
// The total is needed to calculate pagination
|
||||||
let total = total_items.unwrap_or(folders.len());
|
let total = total_items.unwrap_or(folders.len());
|
||||||
|
|
||||||
// Convertir a PaginatedResponseDto
|
// Convert to PaginatedResponseDto
|
||||||
let response = crate::application::dtos::pagination::PaginatedResponseDto::new(
|
let response = crate::application::dtos::pagination::PaginatedResponseDto::new(
|
||||||
folders.into_iter().map(FolderDto::from).collect(),
|
folders.into_iter().map(FolderDto::from).collect(),
|
||||||
pagination.page,
|
pagination.page,
|
||||||
@@ -164,9 +164,9 @@ impl FolderUseCase for FolderService {
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renombra una carpeta
|
/// Renames a folder
|
||||||
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError> {
|
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError> {
|
||||||
// Validación de entrada
|
// Input validation
|
||||||
if dto.name.is_empty() {
|
if dto.name.is_empty() {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::InvalidInput,
|
ErrorKind::InvalidInput,
|
||||||
@@ -175,15 +175,15 @@ impl FolderUseCase for FolderService {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar que la carpeta existe
|
// Verify the folder exists
|
||||||
let existing_folder = self.folder_storage.get_folder(id)
|
let existing_folder = self.folder_storage.get_folder(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for renaming: {}", id, e)))?;
|
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for renaming: {}", id, e)))?;
|
||||||
|
|
||||||
// Crear transacción para renombrar
|
// Create transaction for renaming
|
||||||
let mut transaction = StorageTransaction::new("rename_folder");
|
let mut transaction = StorageTransaction::new("rename_folder");
|
||||||
|
|
||||||
// Operación principal: renombrar carpeta
|
// Main operation: rename folder
|
||||||
// Clone all values to avoid lifetime issues
|
// Clone all values to avoid lifetime issues
|
||||||
let folder_storage = self.folder_storage.clone();
|
let folder_storage = self.folder_storage.clone();
|
||||||
let id_owned = id.to_string();
|
let id_owned = id.to_string();
|
||||||
@@ -200,7 +200,7 @@ impl FolderUseCase for FolderService {
|
|||||||
let id_clone = id.to_string();
|
let id_clone = id.to_string();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// En caso de fallo, restaurar el nombre original
|
// In case of failure, restore the original name
|
||||||
storage.rename_folder(&id_clone, original_name).await
|
storage.rename_folder(&id_clone, original_name).await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(|e| DomainError::new(
|
.map_err(|e| DomainError::new(
|
||||||
@@ -211,13 +211,13 @@ impl FolderUseCase for FolderService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Añadir a la transacción
|
// Add to the transaction
|
||||||
transaction.add_operation(rename_op, rollback_op);
|
transaction.add_operation(rename_op, rollback_op);
|
||||||
|
|
||||||
// Ejecutar transacción
|
// Execute transaction
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
|
|
||||||
// Obtener la carpeta renombrada
|
// Get the renamed folder
|
||||||
let folder = self.folder_storage.get_folder(id)
|
let folder = self.folder_storage.get_folder(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get renamed folder with ID: {}: {}", id, e)))?;
|
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get renamed folder with ID: {}: {}", id, e)))?;
|
||||||
@@ -225,16 +225,16 @@ impl FolderUseCase for FolderService {
|
|||||||
Ok(FolderDto::from(folder))
|
Ok(FolderDto::from(folder))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mueve una carpeta a un nuevo padre
|
/// Moves a folder to a new parent
|
||||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError> {
|
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError> {
|
||||||
// Verificar que la carpeta origen existe
|
// Verify the source folder exists
|
||||||
let source_folder = self.folder_storage.get_folder(id)
|
let source_folder = self.folder_storage.get_folder(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for moving: {}", id, e)))?;
|
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for moving: {}", id, e)))?;
|
||||||
|
|
||||||
// Si se especifica un parent_id, verificar que existe
|
// If a parent_id is specified, verify it exists
|
||||||
if let Some(parent_id) = &dto.parent_id {
|
if let Some(parent_id) = &dto.parent_id {
|
||||||
// Verificar que no estamos intentando mover la carpeta a sí misma o a uno de sus descendientes
|
// Verify we are not trying to move the folder into itself or one of its descendants
|
||||||
if parent_id == id {
|
if parent_id == id {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::InvalidInput,
|
ErrorKind::InvalidInput,
|
||||||
@@ -243,19 +243,19 @@ impl FolderUseCase for FolderService {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar que el destino existe
|
// Verify the destination exists
|
||||||
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
|
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
|
||||||
if !parent_exists {
|
if !parent_exists {
|
||||||
return Err(DomainError::not_found("Folder", parent_id));
|
return Err(DomainError::not_found("Folder", parent_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Idealmente deberíamos verificar toda la jerarquía para evitar ciclos
|
// TODO: Ideally we should verify the entire hierarchy to prevent cycles
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crear transacción para mover
|
// Create transaction for moving
|
||||||
let mut transaction = StorageTransaction::new("move_folder");
|
let mut transaction = StorageTransaction::new("move_folder");
|
||||||
|
|
||||||
// Operación principal: mover carpeta
|
// Main operation: move folder
|
||||||
// Clone all values to avoid lifetime issues
|
// Clone all values to avoid lifetime issues
|
||||||
let folder_storage = self.folder_storage.clone();
|
let folder_storage = self.folder_storage.clone();
|
||||||
let id_owned = id.to_string();
|
let id_owned = id.to_string();
|
||||||
@@ -275,7 +275,7 @@ impl FolderUseCase for FolderService {
|
|||||||
let id_clone = id.to_string();
|
let id_clone = id.to_string();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// En caso de fallo, restaurar la ubicación original
|
// In case of failure, restore the original location
|
||||||
storage.move_folder(&id_clone, original_parent_id.as_deref()).await
|
storage.move_folder(&id_clone, original_parent_id.as_deref()).await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(|e| DomainError::new(
|
.map_err(|e| DomainError::new(
|
||||||
@@ -286,13 +286,13 @@ impl FolderUseCase for FolderService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Añadir a la transacción
|
// Add to the transaction
|
||||||
transaction.add_operation(move_op, rollback_op);
|
transaction.add_operation(move_op, rollback_op);
|
||||||
|
|
||||||
// Ejecutar transacción
|
// Execute transaction
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
|
|
||||||
// Obtener la carpeta movida
|
// Get the moved folder
|
||||||
let folder = self.folder_storage.get_folder(id)
|
let folder = self.folder_storage.get_folder(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get moved folder with ID: {}: {}", id, e)))?;
|
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get moved folder with ID: {}: {}", id, e)))?;
|
||||||
@@ -300,16 +300,16 @@ impl FolderUseCase for FolderService {
|
|||||||
Ok(FolderDto::from(folder))
|
Ok(FolderDto::from(folder))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina una carpeta
|
/// Deletes a folder
|
||||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> {
|
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> {
|
||||||
// Verificar que la carpeta existe
|
// Verify the folder exists
|
||||||
let _folder = self.folder_storage.get_folder(id)
|
let _folder = self.folder_storage.get_folder(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for deletion: {}", id, e)))?;
|
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for deletion: {}", id, e)))?;
|
||||||
|
|
||||||
// En una implementación real, podríamos verificar permisos, dependencias, etc.
|
// In a real implementation, we could verify permissions, dependencies, etc.
|
||||||
|
|
||||||
// Eliminar la carpeta
|
// Delete the folder
|
||||||
self.folder_storage.delete_folder(id)
|
self.folder_storage.delete_folder(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to delete folder with ID: {}: {}", id, e)))
|
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to delete folder with ID: {}: {}", id, e)))
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ use crate::common::errors::{Result, DomainError, ErrorKind};
|
|||||||
use crate::application::ports::recent_ports::{RecentItemsUseCase, RecentItemsRepositoryPort};
|
use crate::application::ports::recent_ports::{RecentItemsUseCase, RecentItemsRepositoryPort};
|
||||||
use crate::application::dtos::recent_dto::RecentItemDto;
|
use crate::application::dtos::recent_dto::RecentItemDto;
|
||||||
|
|
||||||
/// Implementación del caso de uso para gestionar elementos recientes.
|
/// Implementation of the use case for managing recent items.
|
||||||
///
|
///
|
||||||
/// Depende de `RecentItemsRepositoryPort` (outbound port) en lugar
|
/// Depends on `RecentItemsRepositoryPort` (outbound port) instead
|
||||||
/// de acceder directamente a `PgPool`, siguiendo la arquitectura hexagonal.
|
/// of accessing `PgPool` directly, following the hexagonal architecture.
|
||||||
pub struct RecentService {
|
pub struct RecentService {
|
||||||
repo: Arc<dyn RecentItemsRepositoryPort>,
|
repo: Arc<dyn RecentItemsRepositoryPort>,
|
||||||
max_recent_items: i32,
|
max_recent_items: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecentService {
|
impl RecentService {
|
||||||
/// Crear un nuevo servicio de elementos recientes
|
/// Create a new recent items service
|
||||||
pub fn new(repo: Arc<dyn RecentItemsRepositoryPort>, max_recent_items: i32) -> Self {
|
pub fn new(repo: Arc<dyn RecentItemsRepositoryPort>, max_recent_items: i32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
repo,
|
repo,
|
||||||
@@ -26,51 +26,51 @@ impl RecentService {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl RecentItemsUseCase for RecentService {
|
impl RecentItemsUseCase for RecentService {
|
||||||
/// Obtener elementos recientes de un usuario
|
/// Get recent items for a user
|
||||||
async fn get_recent_items(&self, user_id: &str, limit: Option<i32>) -> Result<Vec<RecentItemDto>> {
|
async fn get_recent_items(&self, user_id: &str, limit: Option<i32>) -> Result<Vec<RecentItemDto>> {
|
||||||
info!("Obteniendo elementos recientes para usuario: {}", user_id);
|
info!("Getting recent items for user: {}", user_id);
|
||||||
let limit_value = limit.unwrap_or(self.max_recent_items).min(self.max_recent_items);
|
let limit_value = limit.unwrap_or(self.max_recent_items).min(self.max_recent_items);
|
||||||
let items = self.repo.get_recent_items(user_id, limit_value).await?;
|
let items = self.repo.get_recent_items(user_id, limit_value).await?;
|
||||||
info!("Recuperados {} elementos recientes para usuario {}", items.len(), user_id);
|
info!("Retrieved {} recent items for user {}", items.len(), user_id);
|
||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registrar acceso a un elemento
|
/// Record access to an item
|
||||||
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
|
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);
|
info!("Recording access to {} '{}' for user {}", item_type, item_id, user_id);
|
||||||
|
|
||||||
if item_type != "file" && item_type != "folder" {
|
if item_type != "file" && item_type != "folder" {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::InvalidInput,
|
ErrorKind::InvalidInput,
|
||||||
"RecentItems",
|
"RecentItems",
|
||||||
"El tipo de elemento debe ser 'file' o 'folder'",
|
"Item type must be 'file' or 'folder'",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.repo.upsert_access(user_id, item_id, item_type).await?;
|
self.repo.upsert_access(user_id, item_id, item_type).await?;
|
||||||
self.repo.prune(user_id, self.max_recent_items).await?;
|
self.repo.prune(user_id, self.max_recent_items).await?;
|
||||||
|
|
||||||
info!("Registrado correctamente acceso a {} '{}' para usuario {}", item_type, item_id, user_id);
|
info!("Successfully recorded access to {} '{}' for user {}", item_type, item_id, user_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Eliminar un elemento de recientes
|
/// Remove an item from recent
|
||||||
async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
|
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);
|
info!("Removing {} '{}' from recent for user {}", item_type, item_id, user_id);
|
||||||
let removed = self.repo.remove_item(user_id, item_id, item_type).await?;
|
let removed = self.repo.remove_item(user_id, item_id, item_type).await?;
|
||||||
info!(
|
info!(
|
||||||
"{} {} '{}' de recientes para usuario {}",
|
"{} {} '{}' de recientes para usuario {}",
|
||||||
if removed { "Eliminado correctamente" } else { "No se encontró" },
|
if removed { "Successfully removed" } else { "Not found" },
|
||||||
item_type, item_id, user_id
|
item_type, item_id, user_id
|
||||||
);
|
);
|
||||||
Ok(removed)
|
Ok(removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Limpiar todos los elementos recientes
|
/// Clear all recent items
|
||||||
async fn clear_recent_items(&self, user_id: &str) -> Result<()> {
|
async fn clear_recent_items(&self, user_id: &str) -> Result<()> {
|
||||||
info!("Limpiando todos los elementos recientes para usuario {}", user_id);
|
info!("Clearing all recent items for user {}", user_id);
|
||||||
self.repo.clear_all(user_id).await?;
|
self.repo.clear_all(user_id).await?;
|
||||||
info!("Limpiados todos los elementos recientes para usuario {}", user_id);
|
info!("Cleared all recent items for user {}", user_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,57 +14,57 @@ use crate::application::ports::outbound::FolderStoragePort;
|
|||||||
use crate::application::ports::storage_ports::FileReadPort;
|
use crate::application::ports::storage_ports::FileReadPort;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementación del servicio de búsqueda para archivos y carpetas.
|
* Search service implementation for files and folders.
|
||||||
*
|
*
|
||||||
* Este servicio implementa la funcionalidad de búsqueda avanzada que permite
|
* This service implements the advanced search functionality that allows
|
||||||
* a los usuarios encontrar archivos y carpetas basados en diversos criterios
|
* users to find files and folders based on various criteria
|
||||||
* como nombre, tipo, fecha y tamaño. También incluye una caché para mejorar
|
* such as name, type, date and size. It also includes a cache to improve
|
||||||
* el rendimiento de búsquedas repetidas.
|
* the performance of repeated searches.
|
||||||
*/
|
*/
|
||||||
pub struct SearchService {
|
pub struct SearchService {
|
||||||
/// Repositorio para operaciones con archivos
|
/// Repository for file operations
|
||||||
file_repository: Arc<dyn FileReadPort>,
|
file_repository: Arc<dyn FileReadPort>,
|
||||||
|
|
||||||
/// Repositorio para operaciones con carpetas
|
/// Repository for folder operations
|
||||||
folder_repository: Arc<dyn FolderStoragePort>,
|
folder_repository: Arc<dyn FolderStoragePort>,
|
||||||
|
|
||||||
/// Caché de resultados de búsqueda con tiempo de expiración
|
/// Search results cache with expiration time
|
||||||
search_cache: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
|
search_cache: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
|
||||||
|
|
||||||
/// Duración de validez de la caché en segundos
|
/// Cache validity duration in seconds
|
||||||
cache_ttl: u64,
|
cache_ttl: u64,
|
||||||
|
|
||||||
/// Tamaño máximo de la caché (número de resultados almacenados)
|
/// Maximum cache size (number of stored results)
|
||||||
max_cache_size: usize,
|
max_cache_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clave para la caché de búsqueda
|
/// Key for the search cache
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
struct SearchCacheKey {
|
struct SearchCacheKey {
|
||||||
/// Representación serializada de los criterios de búsqueda
|
/// Serialized representation of the search criteria
|
||||||
criteria_hash: String,
|
criteria_hash: String,
|
||||||
|
|
||||||
/// ID del usuario (para aislar búsquedas entre usuarios)
|
/// User ID (to isolate searches between users)
|
||||||
user_id: String,
|
user_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resultado de búsqueda en caché con tiempo de expiración
|
/// Cached search result with expiration time
|
||||||
struct CachedSearchResult {
|
struct CachedSearchResult {
|
||||||
/// Resultados de la búsqueda
|
/// Search results
|
||||||
results: SearchResultsDto,
|
results: SearchResultsDto,
|
||||||
|
|
||||||
/// Momento en que se creó la entrada de caché
|
/// Time when the cache entry was created
|
||||||
timestamp: Instant,
|
timestamp: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SearchService {
|
impl SearchService {
|
||||||
/**
|
/**
|
||||||
* Crea una nueva instancia del servicio de búsqueda.
|
* Creates a new instance of the search service.
|
||||||
*
|
*
|
||||||
* @param file_repository Repositorio para operaciones con archivos
|
* @param file_repository Repository for file operations
|
||||||
* @param folder_repository Repositorio para operaciones con carpetas
|
* @param folder_repository Repository for folder operations
|
||||||
* @param cache_ttl Tiempo de vida de la caché en segundos (0 para desactivar)
|
* @param cache_ttl Cache time-to-live in seconds (0 to disable)
|
||||||
* @param max_cache_size Tamaño máximo de la caché
|
* @param max_cache_size Maximum cache size
|
||||||
*/
|
*/
|
||||||
pub fn new(
|
pub fn new(
|
||||||
file_repository: Arc<dyn FileReadPort>,
|
file_repository: Arc<dyn FileReadPort>,
|
||||||
@@ -80,7 +80,7 @@ impl SearchService {
|
|||||||
max_cache_size,
|
max_cache_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Iniciar tarea de limpieza de caché si TTL > 0
|
// Start cache cleanup task if TTL > 0
|
||||||
if cache_ttl > 0 {
|
if cache_ttl > 0 {
|
||||||
Self::start_cache_cleanup_task(search_service.search_cache.clone(), cache_ttl);
|
Self::start_cache_cleanup_task(search_service.search_cache.clone(), cache_ttl);
|
||||||
}
|
}
|
||||||
@@ -89,10 +89,10 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inicia una tarea asíncrona para limpiar entradas expiradas de la caché.
|
* Starts an asynchronous task to clean up expired cache entries.
|
||||||
*
|
*
|
||||||
* @param cache_ref Referencia a la caché compartida
|
* @param cache_ref Reference to the shared cache
|
||||||
* @param ttl_seconds TTL en segundos
|
* @param ttl_seconds TTL in seconds
|
||||||
*/
|
*/
|
||||||
fn start_cache_cleanup_task(
|
fn start_cache_cleanup_task(
|
||||||
cache_ref: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
|
cache_ref: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
|
||||||
@@ -105,18 +105,18 @@ impl SearchService {
|
|||||||
loop {
|
loop {
|
||||||
time::sleep(cleanup_interval).await;
|
time::sleep(cleanup_interval).await;
|
||||||
|
|
||||||
// Obtener lock y limpiar entradas expiradas
|
// Acquire lock and clean up expired entries
|
||||||
if let Ok(mut cache) = cache_ref.lock() {
|
if let Ok(mut cache) = cache_ref.lock() {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
||||||
// Identificar entradas expiradas
|
// Identify expired entries
|
||||||
let expired_keys: Vec<SearchCacheKey> = cache
|
let expired_keys: Vec<SearchCacheKey> = cache
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, result)| now.duration_since(result.timestamp) > ttl)
|
.filter(|(_, result)| now.duration_since(result.timestamp) > ttl)
|
||||||
.map(|(key, _)| key.clone())
|
.map(|(key, _)| key.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Eliminar entradas expiradas
|
// Remove expired entries
|
||||||
for key in expired_keys {
|
for key in expired_keys {
|
||||||
cache.remove(&key);
|
cache.remove(&key);
|
||||||
}
|
}
|
||||||
@@ -126,14 +126,14 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crea una clave de caché a partir de los criterios de búsqueda.
|
* Creates a cache key from the search criteria.
|
||||||
*
|
*
|
||||||
* @param criteria Criterios de búsqueda
|
* @param criteria Search criteria
|
||||||
* @param user_id ID del usuario (para aislar caché entre usuarios)
|
* @param user_id User ID (to isolate cache between users)
|
||||||
* @return Clave para la caché
|
* @return Cache key
|
||||||
*/
|
*/
|
||||||
fn create_cache_key(&self, criteria: &SearchCriteriaDto, user_id: &str) -> SearchCacheKey {
|
fn create_cache_key(&self, criteria: &SearchCriteriaDto, user_id: &str) -> SearchCacheKey {
|
||||||
// Serializar criterios para generar un hash
|
// Serialize criteria to generate a hash
|
||||||
let criteria_str = serde_json::to_string(criteria).unwrap_or_default();
|
let criteria_str = serde_json::to_string(criteria).unwrap_or_default();
|
||||||
|
|
||||||
SearchCacheKey {
|
SearchCacheKey {
|
||||||
@@ -143,13 +143,13 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intenta obtener resultados de la caché.
|
* Attempts to retrieve results from the cache.
|
||||||
*
|
*
|
||||||
* @param key Clave de caché
|
* @param key Cache key
|
||||||
* @return Opcionalmente, los resultados si existen y no han expirado
|
* @return Optionally, the results if they exist and have not expired
|
||||||
*/
|
*/
|
||||||
fn get_from_cache(&self, key: &SearchCacheKey) -> Option<SearchResultsDto> {
|
fn get_from_cache(&self, key: &SearchCacheKey) -> Option<SearchResultsDto> {
|
||||||
// Si TTL es 0, la caché está desactivada
|
// If TTL is 0, the cache is disabled
|
||||||
if self.cache_ttl == 0 {
|
if self.cache_ttl == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ impl SearchService {
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let ttl = Duration::from_secs(self.cache_ttl);
|
let ttl = Duration::from_secs(self.cache_ttl);
|
||||||
|
|
||||||
// Comprobar si la entrada ha expirado
|
// Check if the entry has expired
|
||||||
if now.duration_since(cached_result.timestamp) < ttl {
|
if now.duration_since(cached_result.timestamp) < ttl {
|
||||||
return Some(cached_result.results.clone());
|
return Some(cached_result.results.clone());
|
||||||
}
|
}
|
||||||
@@ -170,19 +170,19 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Almacena resultados en la caché.
|
* Stores results in the cache.
|
||||||
*
|
*
|
||||||
* @param key Clave de caché
|
* @param key Cache key
|
||||||
* @param results Resultados a almacenar
|
* @param results Results to store
|
||||||
*/
|
*/
|
||||||
fn store_in_cache(&self, key: SearchCacheKey, results: SearchResultsDto) {
|
fn store_in_cache(&self, key: SearchCacheKey, results: SearchResultsDto) {
|
||||||
// Si TTL es 0, la caché está desactivada
|
// If TTL is 0, the cache is disabled
|
||||||
if self.cache_ttl == 0 {
|
if self.cache_ttl == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(mut cache) = self.search_cache.lock() {
|
if let Ok(mut cache) = self.search_cache.lock() {
|
||||||
// Si la caché está llena, eliminar la entrada más antigua
|
// If the cache is full, remove the oldest entry
|
||||||
if cache.len() >= self.max_cache_size {
|
if cache.len() >= self.max_cache_size {
|
||||||
if let Some((oldest_key, _)) = cache
|
if let Some((oldest_key, _)) = cache
|
||||||
.iter()
|
.iter()
|
||||||
@@ -192,7 +192,7 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Almacenar el nuevo resultado
|
// Store the new result
|
||||||
cache.insert(key, CachedSearchResult {
|
cache.insert(key, CachedSearchResult {
|
||||||
results,
|
results,
|
||||||
timestamp: Instant::now(),
|
timestamp: Instant::now(),
|
||||||
@@ -201,35 +201,35 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filtra archivos según los criterios de búsqueda.
|
* Filters files according to the search criteria.
|
||||||
*
|
*
|
||||||
* @param files Lista de archivos a filtrar
|
* @param files List of files to filter
|
||||||
* @param criteria Criterios de búsqueda
|
* @param criteria Search criteria
|
||||||
* @return Archivos que cumplen con los criterios
|
* @return Files that match the criteria
|
||||||
*/
|
*/
|
||||||
fn filter_files(&self, files: Vec<FileDto>, criteria: &SearchCriteriaDto) -> Vec<FileDto> {
|
fn filter_files(&self, files: Vec<FileDto>, criteria: &SearchCriteriaDto) -> Vec<FileDto> {
|
||||||
files.into_iter()
|
files.into_iter()
|
||||||
.filter(|file| {
|
.filter(|file| {
|
||||||
// Filtrar por nombre
|
// Filter by name
|
||||||
if let Some(name_query) = &criteria.name_contains {
|
if let Some(name_query) = &criteria.name_contains {
|
||||||
if !file.name.to_lowercase().contains(&name_query.to_lowercase()) {
|
if !file.name.to_lowercase().contains(&name_query.to_lowercase()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrar por tipo de archivo (extensión)
|
// Filter by file type (extension)
|
||||||
if let Some(file_types) = &criteria.file_types {
|
if let Some(file_types) = &criteria.file_types {
|
||||||
if let Some(extension) = file.name.split('.').last() {
|
if let Some(extension) = file.name.split('.').last() {
|
||||||
if !file_types.iter().any(|ext| ext.eq_ignore_ascii_case(extension)) {
|
if !file_types.iter().any(|ext| ext.eq_ignore_ascii_case(extension)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No tiene extensión
|
// Has no extension
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrar por fecha de creación
|
// Filter by creation date
|
||||||
if let Some(created_after) = criteria.created_after {
|
if let Some(created_after) = criteria.created_after {
|
||||||
if file.created_at < created_after {
|
if file.created_at < created_after {
|
||||||
return false;
|
return false;
|
||||||
@@ -242,7 +242,7 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrar por fecha de modificación
|
// Filter by modification date
|
||||||
if let Some(modified_after) = criteria.modified_after {
|
if let Some(modified_after) = criteria.modified_after {
|
||||||
if file.modified_at < modified_after {
|
if file.modified_at < modified_after {
|
||||||
return false;
|
return false;
|
||||||
@@ -255,7 +255,7 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrar por tamaño
|
// Filter by size
|
||||||
if let Some(min_size) = criteria.min_size {
|
if let Some(min_size) = criteria.min_size {
|
||||||
if file.size < min_size {
|
if file.size < min_size {
|
||||||
return false;
|
return false;
|
||||||
@@ -274,23 +274,23 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filtra carpetas según los criterios de búsqueda.
|
* Filters folders according to the search criteria.
|
||||||
*
|
*
|
||||||
* @param folders Lista de carpetas a filtrar
|
* @param folders List of folders to filter
|
||||||
* @param criteria Criterios de búsqueda
|
* @param criteria Search criteria
|
||||||
* @return Carpetas que cumplen con los criterios
|
* @return Folders that match the criteria
|
||||||
*/
|
*/
|
||||||
fn filter_folders(&self, folders: Vec<FolderDto>, criteria: &SearchCriteriaDto) -> Vec<FolderDto> {
|
fn filter_folders(&self, folders: Vec<FolderDto>, criteria: &SearchCriteriaDto) -> Vec<FolderDto> {
|
||||||
folders.into_iter()
|
folders.into_iter()
|
||||||
.filter(|folder| {
|
.filter(|folder| {
|
||||||
// Filtrar por nombre
|
// Filter by name
|
||||||
if let Some(name_query) = &criteria.name_contains {
|
if let Some(name_query) = &criteria.name_contains {
|
||||||
if !folder.name.to_lowercase().contains(&name_query.to_lowercase()) {
|
if !folder.name.to_lowercase().contains(&name_query.to_lowercase()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrar por fecha de creación
|
// Filter by creation date
|
||||||
if let Some(created_after) = criteria.created_after {
|
if let Some(created_after) = criteria.created_after {
|
||||||
if folder.created_at < created_after {
|
if folder.created_at < created_after {
|
||||||
return false;
|
return false;
|
||||||
@@ -303,7 +303,7 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrar por fecha de modificación
|
// Filter by modification date
|
||||||
if let Some(modified_after) = criteria.modified_after {
|
if let Some(modified_after) = criteria.modified_after {
|
||||||
if folder.modified_at < modified_after {
|
if folder.modified_at < modified_after {
|
||||||
return false;
|
return false;
|
||||||
@@ -322,12 +322,12 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementación de la búsqueda recursiva a través de carpetas.
|
* Implementation of recursive search through folders.
|
||||||
*
|
*
|
||||||
* @param current_folder_id ID de la carpeta actual
|
* @param current_folder_id ID of the current folder
|
||||||
* @param criteria Criterios de búsqueda
|
* @param criteria Search criteria
|
||||||
* @param found_files Archivos encontrados hasta ahora
|
* @param found_files Files found so far
|
||||||
* @param found_folders Carpetas encontradas hasta ahora
|
* @param found_folders Folders found so far
|
||||||
*/
|
*/
|
||||||
async fn search_recursive(
|
async fn search_recursive(
|
||||||
&self,
|
&self,
|
||||||
@@ -337,31 +337,31 @@ impl SearchService {
|
|||||||
found_folders: &mut Vec<FolderDto>,
|
found_folders: &mut Vec<FolderDto>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// Listar archivos en la carpeta actual
|
// List files in the current folder
|
||||||
let files = self.file_repository.list_files(current_folder_id).await?;
|
let files = self.file_repository.list_files(current_folder_id).await?;
|
||||||
|
|
||||||
// Filtrar archivos según criterios y agregarlos a los resultados
|
// Filter files according to criteria and add them to the results
|
||||||
let filtered_files = self.filter_files(
|
let filtered_files = self.filter_files(
|
||||||
files.into_iter().map(FileDto::from).collect(),
|
files.into_iter().map(FileDto::from).collect(),
|
||||||
criteria
|
criteria
|
||||||
);
|
);
|
||||||
found_files.extend(filtered_files);
|
found_files.extend(filtered_files);
|
||||||
|
|
||||||
// Si la búsqueda es recursiva, procesar subcarpetas
|
// If the search is recursive, process subfolders
|
||||||
if criteria.recursive {
|
if criteria.recursive {
|
||||||
// Listar subcarpetas
|
// List subfolders
|
||||||
let folders = self.folder_repository.list_folders(current_folder_id).await?;
|
let folders = self.folder_repository.list_folders(current_folder_id).await?;
|
||||||
|
|
||||||
// Filtrar carpetas según criterios y agregarlas a los resultados
|
// Filter folders according to criteria and add them to the results
|
||||||
let filtered_folders: Vec<FolderDto> = self.filter_folders(
|
let filtered_folders: Vec<FolderDto> = self.filter_folders(
|
||||||
folders.into_iter().map(FolderDto::from).collect(),
|
folders.into_iter().map(FolderDto::from).collect(),
|
||||||
criteria
|
criteria
|
||||||
);
|
);
|
||||||
|
|
||||||
// Añadir las carpetas filtradas a los resultados
|
// Add filtered folders to the results
|
||||||
found_folders.extend(filtered_folders.iter().cloned());
|
found_folders.extend(filtered_folders.iter().cloned());
|
||||||
|
|
||||||
// Buscar recursivamente en cada subcarpeta
|
// Search recursively in each subfolder
|
||||||
for folder in filtered_folders {
|
for folder in filtered_folders {
|
||||||
self.search_recursive(
|
self.search_recursive(
|
||||||
Some(&folder.id),
|
Some(&folder.id),
|
||||||
@@ -380,26 +380,26 @@ impl SearchService {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl SearchUseCase for SearchService {
|
impl SearchUseCase for SearchService {
|
||||||
/**
|
/**
|
||||||
* Realiza una búsqueda basada en los criterios especificados.
|
* Performs a search based on the specified criteria.
|
||||||
*
|
*
|
||||||
* @param criteria Criterios de búsqueda
|
* @param criteria Search criteria
|
||||||
* @return Resultados de la búsqueda
|
* @return Search results
|
||||||
*/
|
*/
|
||||||
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto> {
|
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto> {
|
||||||
// TODO: Obtener ID de usuario del contexto de autenticación
|
// TODO: Get user ID from the authentication context
|
||||||
let user_id = "default-user";
|
let user_id = "default-user";
|
||||||
let cache_key = self.create_cache_key(&criteria, user_id);
|
let cache_key = self.create_cache_key(&criteria, user_id);
|
||||||
|
|
||||||
// Intentar obtener resultados de la caché
|
// Try to get results from the cache
|
||||||
if let Some(cached_results) = self.get_from_cache(&cache_key) {
|
if let Some(cached_results) = self.get_from_cache(&cache_key) {
|
||||||
return Ok(cached_results);
|
return Ok(cached_results);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inicializar colecciones para resultados
|
// Initialize collections for results
|
||||||
let mut found_files: Vec<FileDto> = Vec::new();
|
let mut found_files: Vec<FileDto> = Vec::new();
|
||||||
let mut found_folders: Vec<FolderDto> = Vec::new();
|
let mut found_folders: Vec<FolderDto> = Vec::new();
|
||||||
|
|
||||||
// Realizar búsqueda en la carpeta especificada o en la raíz
|
// Perform search in the specified folder or at the root
|
||||||
self.search_recursive(
|
self.search_recursive(
|
||||||
criteria.folder_id.as_deref(),
|
criteria.folder_id.as_deref(),
|
||||||
&criteria,
|
&criteria,
|
||||||
@@ -407,29 +407,29 @@ impl SearchUseCase for SearchService {
|
|||||||
&mut found_folders,
|
&mut found_folders,
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
// Aplicar paginación
|
// Apply pagination
|
||||||
let total_count = found_files.len() + found_folders.len();
|
let total_count = found_files.len() + found_folders.len();
|
||||||
|
|
||||||
// Ordenar por relevancia o fecha según criterios
|
// Sort by relevance or date according to criteria
|
||||||
// Por defecto, ordenamos por fecha de modificación (más reciente primero)
|
// By default, sort by modification date (most recent first)
|
||||||
found_files.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
|
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));
|
found_folders.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
|
||||||
|
|
||||||
// Aplicar límite y offset para paginación
|
// Apply limit and offset for pagination
|
||||||
let start_idx = criteria.offset.min(total_count);
|
let start_idx = criteria.offset.min(total_count);
|
||||||
let end_idx = (criteria.offset + criteria.limit).min(total_count);
|
let end_idx = (criteria.offset + criteria.limit).min(total_count);
|
||||||
|
|
||||||
let paginated_items: Vec<(bool, usize)> = (start_idx..end_idx)
|
let paginated_items: Vec<(bool, usize)> = (start_idx..end_idx)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
if i < found_folders.len() {
|
if i < found_folders.len() {
|
||||||
(true, i) // Es una carpeta
|
(true, i) // It's a folder
|
||||||
} else {
|
} else {
|
||||||
(false, i - found_folders.len()) // Es un archivo
|
(false, i - found_folders.len()) // It's a file
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Extraer elementos paginados
|
// Extract paginated items
|
||||||
let mut paginated_folders = Vec::new();
|
let mut paginated_folders = Vec::new();
|
||||||
let mut paginated_files = Vec::new();
|
let mut paginated_files = Vec::new();
|
||||||
|
|
||||||
@@ -445,7 +445,7 @@ impl SearchUseCase for SearchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crear objeto de resultados
|
// Create results object
|
||||||
let search_results = SearchResultsDto::new(
|
let search_results = SearchResultsDto::new(
|
||||||
paginated_files,
|
paginated_files,
|
||||||
paginated_folders,
|
paginated_folders,
|
||||||
@@ -454,16 +454,16 @@ impl SearchUseCase for SearchService {
|
|||||||
Some(total_count),
|
Some(total_count),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Almacenar en caché
|
// Store in cache
|
||||||
self.store_in_cache(cache_key, search_results.clone());
|
self.store_in_cache(cache_key, search_results.clone());
|
||||||
|
|
||||||
Ok(search_results)
|
Ok(search_results)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limpia la caché de resultados de búsqueda.
|
* Clears the search results cache.
|
||||||
*
|
*
|
||||||
* @return Resultado indicando éxito
|
* @return Result indicating success
|
||||||
*/
|
*/
|
||||||
async fn clear_search_cache(&self) -> Result<()> {
|
async fn clear_search_cache(&self) -> Result<()> {
|
||||||
if let Ok(mut cache) = self.search_cache.lock() {
|
if let Ok(mut cache) = self.search_cache.lock() {
|
||||||
@@ -473,9 +473,9 @@ impl SearchUseCase for SearchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementar el caso de uso de prueba (stub)
|
// Implement the test use case (stub)
|
||||||
impl SearchService {
|
impl SearchService {
|
||||||
/// Crea una versión stub del servicio para pruebas
|
/// Creates a stub version of the service for testing
|
||||||
pub fn new_stub() -> impl SearchUseCase {
|
pub fn new_stub() -> impl SearchUseCase {
|
||||||
struct SearchServiceStub;
|
struct SearchServiceStub;
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ impl ShareService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifica que el elemento a compartir existe
|
/// Verifies that the item to share exists
|
||||||
async fn verify_item_exists(
|
async fn verify_item_exists(
|
||||||
&self,
|
&self,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
@@ -89,13 +89,13 @@ impl ShareService {
|
|||||||
match item_type {
|
match item_type {
|
||||||
ShareItemType::File => {
|
ShareItemType::File => {
|
||||||
self.file_repository
|
self.file_repository
|
||||||
.get_file(item_id) // Usando el método correcto del trait FileStoragePort
|
.get_file(item_id) // Using the correct method from the FileStoragePort trait
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ShareServiceError::ItemNotFound(format!("File with ID {} not found", item_id)))?;
|
.map_err(|_| ShareServiceError::ItemNotFound(format!("File with ID {} not found", item_id)))?;
|
||||||
}
|
}
|
||||||
ShareItemType::Folder => {
|
ShareItemType::Folder => {
|
||||||
self.folder_repository
|
self.folder_repository
|
||||||
.get_folder(item_id) // Usando el método correcto del trait FolderStoragePort
|
.get_folder(item_id) // Using the correct method from the FolderStoragePort trait
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ShareServiceError::ItemNotFound(format!("Folder with ID {} not found", item_id)))?;
|
.map_err(|_| ShareServiceError::ItemNotFound(format!("Folder with ID {} not found", item_id)))?;
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ impl ShareService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hash de contraseña usando Argon2id (resistente a timing attacks y GPU attacks)
|
/// Password hash using Argon2id (resistant to timing attacks and GPU attacks)
|
||||||
fn hash_password(&self, password: &str) -> String {
|
fn hash_password(&self, password: &str) -> String {
|
||||||
use argon2::{Argon2, PasswordHasher};
|
use argon2::{Argon2, PasswordHasher};
|
||||||
use argon2::password_hash::SaltString;
|
use argon2::password_hash::SaltString;
|
||||||
@@ -126,20 +126,20 @@ impl ShareUseCase for ShareService {
|
|||||||
user_id: &str,
|
user_id: &str,
|
||||||
dto: CreateShareDto,
|
dto: CreateShareDto,
|
||||||
) -> Result<ShareDto, DomainError> {
|
) -> Result<ShareDto, DomainError> {
|
||||||
// Convertir el tipo de elemento
|
// Convert the item type
|
||||||
let item_type = ShareItemType::try_from(dto.item_type.as_str())
|
let item_type = ShareItemType::try_from(dto.item_type.as_str())
|
||||||
.map_err(|e| ShareServiceError::InvalidItemType(e.to_string()))?;
|
.map_err(|e| ShareServiceError::InvalidItemType(e.to_string()))?;
|
||||||
|
|
||||||
// Verificar que el elemento existe
|
// Verify that the item exists
|
||||||
self.verify_item_exists(&dto.item_id, &item_type).await?;
|
self.verify_item_exists(&dto.item_id, &item_type).await?;
|
||||||
|
|
||||||
// Convertir el DTO de permisos si existe
|
// Convert the permissions DTO if it exists
|
||||||
let permissions = dto.permissions.map(|p| p.to_entity());
|
let permissions = dto.permissions.map(|p| p.to_entity());
|
||||||
|
|
||||||
// Hash de contraseña si existe
|
// Hash the password if provided
|
||||||
let password_hash = dto.password.map(|p| self.hash_password(&p));
|
let password_hash = dto.password.map(|p| self.hash_password(&p));
|
||||||
|
|
||||||
// Crear la entidad Share
|
// Create the Share entity
|
||||||
let share = Share::new(
|
let share = Share::new(
|
||||||
dto.item_id.clone(),
|
dto.item_id.clone(),
|
||||||
item_type,
|
item_type,
|
||||||
@@ -150,48 +150,48 @@ impl ShareUseCase for ShareService {
|
|||||||
)
|
)
|
||||||
.map_err(|e| ShareServiceError::Validation(e.to_string()))?;
|
.map_err(|e| ShareServiceError::Validation(e.to_string()))?;
|
||||||
|
|
||||||
// Guardar en el repositorio
|
// Save to the repository
|
||||||
let saved_share = self
|
let saved_share = self
|
||||||
.share_repository
|
.share_repository
|
||||||
.save_share(&share)
|
.save_share(&share)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
||||||
|
|
||||||
// Convertir la entidad a DTO para la respuesta
|
// Convert the entity to DTO for the response
|
||||||
Ok(ShareDto::from_entity(&saved_share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
Ok(ShareDto::from_entity(&saved_share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_shared_link(&self, id: &str) -> Result<ShareDto, DomainError> {
|
async fn get_shared_link(&self, id: &str) -> Result<ShareDto, DomainError> {
|
||||||
// Buscar el enlace compartido por su ID
|
// Find the shared link by its ID
|
||||||
let share = self
|
let share = self
|
||||||
.share_repository
|
.share_repository
|
||||||
.find_share_by_id(id)
|
.find_share_by_id(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?;
|
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?;
|
||||||
|
|
||||||
// Verificar si ha expirado
|
// Check if it has expired
|
||||||
if share.is_expired() {
|
if share.is_expired() {
|
||||||
return Err(ShareServiceError::Expired.into());
|
return Err(ShareServiceError::Expired.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convertir la entidad a DTO para la respuesta
|
// Convert the entity to DTO for the response
|
||||||
Ok(ShareDto::from_entity(&share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
Ok(ShareDto::from_entity(&share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError> {
|
async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError> {
|
||||||
// Buscar el enlace compartido por su token
|
// Find the shared link by its token
|
||||||
let share = self
|
let share = self
|
||||||
.share_repository
|
.share_repository
|
||||||
.find_share_by_token(token)
|
.find_share_by_token(token)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
|
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
|
||||||
|
|
||||||
// Verificar si ha expirado
|
// Check if it has expired
|
||||||
if share.is_expired() {
|
if share.is_expired() {
|
||||||
return Err(ShareServiceError::Expired.into());
|
return Err(ShareServiceError::Expired.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convertir la entidad a DTO para la respuesta
|
// Convert the entity to DTO for the response
|
||||||
Ok(ShareDto::from_entity(&share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
Ok(ShareDto::from_entity(&share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,17 +200,17 @@ impl ShareUseCase for ShareService {
|
|||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &ShareItemType,
|
item_type: &ShareItemType,
|
||||||
) -> Result<Vec<ShareDto>, DomainError> {
|
) -> Result<Vec<ShareDto>, DomainError> {
|
||||||
// Buscar todos los enlaces compartidos para el elemento
|
// Find all shared links for the item
|
||||||
let shares = self
|
let shares = self
|
||||||
.share_repository
|
.share_repository
|
||||||
.find_shares_by_item(item_id, item_type)
|
.find_shares_by_item(item_id, item_type)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
||||||
|
|
||||||
// Filtrar los enlaces expirados
|
// Filter out expired links
|
||||||
let active_shares: Vec<Share> = shares.into_iter().filter(|s| !s.is_expired()).collect();
|
let active_shares: Vec<Share> = shares.into_iter().filter(|s| !s.is_expired()).collect();
|
||||||
|
|
||||||
// Convertir las entidades a DTOs para la respuesta
|
// Convert the entities to DTOs for the response
|
||||||
let share_dtos = active_shares
|
let share_dtos = active_shares
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
||||||
@@ -224,14 +224,14 @@ impl ShareUseCase for ShareService {
|
|||||||
id: &str,
|
id: &str,
|
||||||
dto: UpdateShareDto,
|
dto: UpdateShareDto,
|
||||||
) -> Result<ShareDto, DomainError> {
|
) -> Result<ShareDto, DomainError> {
|
||||||
// Buscar el enlace compartido existente
|
// Find the existing shared link
|
||||||
let mut share = self
|
let mut share = self
|
||||||
.share_repository
|
.share_repository
|
||||||
.find_share_by_id(id)
|
.find_share_by_id(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?;
|
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?;
|
||||||
|
|
||||||
// Actualizar permisos si se proporcionan
|
// Update permissions if provided
|
||||||
if let Some(permissions_dto) = dto.permissions {
|
if let Some(permissions_dto) = dto.permissions {
|
||||||
let permissions = SharePermissions::new(
|
let permissions = SharePermissions::new(
|
||||||
permissions_dto.read,
|
permissions_dto.read,
|
||||||
@@ -241,7 +241,7 @@ impl ShareUseCase for ShareService {
|
|||||||
share = share.with_permissions(permissions);
|
share = share.with_permissions(permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar contraseña si se proporciona
|
// Update password if provided
|
||||||
if let Some(password) = dto.password {
|
if let Some(password) = dto.password {
|
||||||
let password_hash = if password.is_empty() {
|
let password_hash = if password.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -251,24 +251,24 @@ impl ShareUseCase for ShareService {
|
|||||||
share = share.with_password(password_hash);
|
share = share.with_password(password_hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar fecha de expiración si se proporciona
|
// Update expiration date if provided
|
||||||
if dto.expires_at.is_some() {
|
if dto.expires_at.is_some() {
|
||||||
share = share.with_expiration(dto.expires_at);
|
share = share.with_expiration(dto.expires_at);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guardar los cambios
|
// Save the changes
|
||||||
let updated_share = self
|
let updated_share = self
|
||||||
.share_repository
|
.share_repository
|
||||||
.update_share(&share)
|
.update_share(&share)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
||||||
|
|
||||||
// Convertir la entidad a DTO para la respuesta
|
// Convert the entity to DTO for the response
|
||||||
Ok(ShareDto::from_entity(&updated_share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
Ok(ShareDto::from_entity(&updated_share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_shared_link(&self, id: &str) -> Result<(), DomainError> {
|
async fn delete_shared_link(&self, id: &str) -> Result<(), DomainError> {
|
||||||
// Eliminar el enlace compartido
|
// Delete the shared link
|
||||||
self.share_repository
|
self.share_repository
|
||||||
.delete_share(id)
|
.delete_share(id)
|
||||||
.await
|
.await
|
||||||
@@ -283,23 +283,23 @@ impl ShareUseCase for ShareService {
|
|||||||
page: usize,
|
page: usize,
|
||||||
per_page: usize,
|
per_page: usize,
|
||||||
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
|
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
|
||||||
// Calcular offset para paginación
|
// Calculate offset for pagination
|
||||||
let offset = (page - 1) * per_page;
|
let offset = (page - 1) * per_page;
|
||||||
|
|
||||||
// Buscar los enlaces compartidos del usuario
|
// Find the user's shared links
|
||||||
let (shares, total) = self
|
let (shares, total) = self
|
||||||
.share_repository
|
.share_repository
|
||||||
.find_shares_by_user(user_id, offset, per_page)
|
.find_shares_by_user(user_id, offset, per_page)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
||||||
|
|
||||||
// Convertir las entidades a DTOs
|
// Convert the entities to DTOs
|
||||||
let share_dtos: Vec<ShareDto> = shares
|
let share_dtos: Vec<ShareDto> = shares
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Crear el resultado paginado
|
// Create the paginated result
|
||||||
let paginated = PaginatedResponseDto::new(
|
let paginated = PaginatedResponseDto::new(
|
||||||
share_dtos,
|
share_dtos,
|
||||||
page,
|
page,
|
||||||
@@ -315,19 +315,19 @@ impl ShareUseCase for ShareService {
|
|||||||
token: &str,
|
token: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<bool, DomainError> {
|
) -> Result<bool, DomainError> {
|
||||||
// Buscar el enlace compartido por su token
|
// Find the shared link by its token
|
||||||
let share = self
|
let share = self
|
||||||
.share_repository
|
.share_repository
|
||||||
.find_share_by_token(token)
|
.find_share_by_token(token)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
|
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
|
||||||
|
|
||||||
// Verificar si ha expirado
|
// Check if it has expired
|
||||||
if share.is_expired() {
|
if share.is_expired() {
|
||||||
return Err(ShareServiceError::Expired.into());
|
return Err(ShareServiceError::Expired.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar la contraseña usando el port de infraestructura
|
// Verify the password using the infrastructure port
|
||||||
match share.password_hash() {
|
match share.password_hash() {
|
||||||
Some(hash) => {
|
Some(hash) => {
|
||||||
self.password_hasher.verify_password(password, hash)
|
self.password_hasher.verify_password(password, hash)
|
||||||
@@ -337,22 +337,22 @@ impl ShareUseCase for ShareService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn register_shared_link_access(&self, token: &str) -> Result<(), DomainError> {
|
async fn register_shared_link_access(&self, token: &str) -> Result<(), DomainError> {
|
||||||
// Buscar el enlace compartido por su token
|
// Find the shared link by its token
|
||||||
let share = self
|
let share = self
|
||||||
.share_repository
|
.share_repository
|
||||||
.find_share_by_token(token)
|
.find_share_by_token(token)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
|
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
|
||||||
|
|
||||||
// Verificar si ha expirado
|
// Check if it has expired
|
||||||
if share.is_expired() {
|
if share.is_expired() {
|
||||||
return Err(ShareServiceError::Expired.into());
|
return Err(ShareServiceError::Expired.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Incrementar el contador de accesos
|
// Increment the access counter
|
||||||
let updated_share = share.increment_access_count();
|
let updated_share = share.increment_access_count();
|
||||||
|
|
||||||
// Guardar los cambios
|
// Save the changes
|
||||||
self.share_repository
|
self.share_repository
|
||||||
.update_share(&updated_share)
|
.update_share(&updated_share)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -7,69 +7,69 @@ use crate::domain::entities::folder::Folder;
|
|||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::StoragePath;
|
||||||
use crate::application::ports::outbound::{IdMappingPort, StoragePort, FolderStoragePort};
|
use crate::application::ports::outbound::{IdMappingPort, StoragePort, FolderStoragePort};
|
||||||
|
|
||||||
/// Errores específicos del mediador de almacenamiento
|
/// Storage mediator specific errors
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum StorageMediatorError {
|
pub enum StorageMediatorError {
|
||||||
#[error("Entidad no encontrada: {0}")]
|
#[error("Entity not found: {0}")]
|
||||||
NotFound(String),
|
NotFound(String),
|
||||||
|
|
||||||
#[error("Entidad ya existe: {0}")]
|
#[error("Entity already exists: {0}")]
|
||||||
AlreadyExists(String),
|
AlreadyExists(String),
|
||||||
|
|
||||||
#[error("Ruta inválida: {0}")]
|
#[error("Invalid path: {0}")]
|
||||||
InvalidPath(String),
|
InvalidPath(String),
|
||||||
|
|
||||||
#[error("Error de acceso: {0}")]
|
#[error("Access error: {0}")]
|
||||||
AccessError(String),
|
AccessError(String),
|
||||||
|
|
||||||
#[error("Error interno: {0}")]
|
#[error("Internal error: {0}")]
|
||||||
InternalError(String),
|
InternalError(String),
|
||||||
|
|
||||||
#[error("Error de dominio: {0}")]
|
#[error("Domain error: {0}")]
|
||||||
DomainError(#[from] crate::common::errors::DomainError),
|
DomainError(#[from] crate::common::errors::DomainError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tipo de resultado para las operaciones del mediador
|
/// Result type for mediator operations
|
||||||
pub type StorageMediatorResult<T> = Result<T, StorageMediatorError>;
|
pub type StorageMediatorResult<T> = Result<T, StorageMediatorError>;
|
||||||
|
|
||||||
/// Interfaz del servicio mediador entre repositorios de archivos y carpetas
|
/// Interface for the mediator service between file and folder repositories
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait StorageMediator: Send + Sync + 'static {
|
pub trait StorageMediator: Send + Sync + 'static {
|
||||||
/// Obtiene la ruta de una carpeta por su ID
|
/// Gets the path of a folder by its ID
|
||||||
async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf>;
|
async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf>;
|
||||||
|
|
||||||
/// Obtiene la ruta de dominio de una carpeta por su ID
|
/// Gets the domain path of a folder by its ID
|
||||||
async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath>;
|
async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath>;
|
||||||
|
|
||||||
/// Obtiene todos los detalles de una carpeta por su ID
|
/// Gets all details of a folder by its ID
|
||||||
async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder>;
|
async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder>;
|
||||||
|
|
||||||
/// Verifica si existe un archivo en una ruta específica
|
/// Checks if a file exists at a specific path
|
||||||
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
|
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
|
||||||
|
|
||||||
/// Verifica si existe un archivo en una ruta de dominio específica
|
/// Checks if a file exists at a specific domain path
|
||||||
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
|
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
|
||||||
|
|
||||||
/// Verifica si existe una carpeta en una ruta específica
|
/// Checks if a folder exists at a specific path
|
||||||
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
|
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
|
||||||
|
|
||||||
/// Verifica si existe una carpeta en una ruta de dominio específica
|
/// Checks if a folder exists at a specific domain path
|
||||||
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
|
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>;
|
||||||
|
|
||||||
/// Resuelve una ruta relativa a absoluta (legacy)
|
/// Resolves a relative path to absolute (legacy)
|
||||||
fn resolve_path(&self, relative_path: &Path) -> PathBuf;
|
fn resolve_path(&self, relative_path: &Path) -> PathBuf;
|
||||||
|
|
||||||
/// Resuelve una ruta de dominio a una ruta física absoluta
|
/// Resolves a domain path to an absolute physical path
|
||||||
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf;
|
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf;
|
||||||
|
|
||||||
/// Crea un directorio si no existe (legacy)
|
/// Creates a directory if it does not exist (legacy)
|
||||||
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()>;
|
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()>;
|
||||||
|
|
||||||
/// Crea un directorio si no existe
|
/// Creates a directory if it does not exist
|
||||||
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()>;
|
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementación concreta del mediador de almacenamiento
|
/// Concrete implementation of the storage mediator
|
||||||
pub struct FileSystemStorageMediator {
|
pub struct FileSystemStorageMediator {
|
||||||
pub folder_storage_port: Arc<dyn FolderStoragePort>,
|
pub folder_storage_port: Arc<dyn FolderStoragePort>,
|
||||||
pub path_service: Arc<dyn StoragePort>,
|
pub path_service: Arc<dyn StoragePort>,
|
||||||
@@ -189,7 +189,7 @@ impl StorageMediator for FileSystemStorageMediator {
|
|||||||
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||||
let abs_path = self.resolve_path(path);
|
let abs_path = self.resolve_path(path);
|
||||||
|
|
||||||
// Verificar si existe como archivo (no como directorio)
|
// Check if it exists as a file (not as a directory)
|
||||||
let exists = abs_path.exists() && abs_path.is_file();
|
let exists = abs_path.exists() && abs_path.is_file();
|
||||||
|
|
||||||
Ok(exists)
|
Ok(exists)
|
||||||
@@ -198,7 +198,7 @@ impl StorageMediator for FileSystemStorageMediator {
|
|||||||
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||||
let abs_path = self.resolve_storage_path(storage_path);
|
let abs_path = self.resolve_storage_path(storage_path);
|
||||||
|
|
||||||
// Verificar si existe como archivo (no como directorio)
|
// Check if it exists as a file (not as a directory)
|
||||||
let exists = abs_path.exists() && abs_path.is_file();
|
let exists = abs_path.exists() && abs_path.is_file();
|
||||||
|
|
||||||
Ok(exists)
|
Ok(exists)
|
||||||
@@ -207,7 +207,7 @@ impl StorageMediator for FileSystemStorageMediator {
|
|||||||
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||||
let abs_path = self.resolve_path(path);
|
let abs_path = self.resolve_path(path);
|
||||||
|
|
||||||
// Verificar si existe como directorio
|
// Check if it exists as a directory
|
||||||
let exists = abs_path.exists() && abs_path.is_dir();
|
let exists = abs_path.exists() && abs_path.is_dir();
|
||||||
|
|
||||||
Ok(exists)
|
Ok(exists)
|
||||||
@@ -216,7 +216,7 @@ impl StorageMediator for FileSystemStorageMediator {
|
|||||||
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||||
let abs_path = self.resolve_storage_path(storage_path);
|
let abs_path = self.resolve_storage_path(storage_path);
|
||||||
|
|
||||||
// Verificar si existe como directorio
|
// Check if it exists as a directory
|
||||||
let exists = abs_path.exists() && abs_path.is_dir();
|
let exists = abs_path.exists() && abs_path.is_dir();
|
||||||
|
|
||||||
Ok(exists)
|
Ok(exists)
|
||||||
@@ -236,13 +236,13 @@ impl StorageMediator for FileSystemStorageMediator {
|
|||||||
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
|
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
|
||||||
let abs_path = self.resolve_path(path);
|
let abs_path = self.resolve_path(path);
|
||||||
|
|
||||||
// Crear directorios si no existen
|
// Create directories if they don't exist
|
||||||
if !abs_path.exists() {
|
if !abs_path.exists() {
|
||||||
tokio::fs::create_dir_all(&abs_path).await
|
tokio::fs::create_dir_all(&abs_path).await
|
||||||
.map_err(|e| StorageMediatorError::AccessError(format!("No se pudo crear el directorio: {}", e)))?;
|
.map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?;
|
||||||
} else if !abs_path.is_dir() {
|
} else if !abs_path.is_dir() {
|
||||||
return Err(StorageMediatorError::InvalidPath(
|
return Err(StorageMediatorError::InvalidPath(
|
||||||
format!("La ruta existe pero no es un directorio: {}", abs_path.display())
|
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,13 +252,13 @@ impl StorageMediator for FileSystemStorageMediator {
|
|||||||
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> {
|
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> {
|
||||||
let abs_path = self.resolve_storage_path(storage_path);
|
let abs_path = self.resolve_storage_path(storage_path);
|
||||||
|
|
||||||
// Crear directorios si no existen
|
// Create directories if they don't exist
|
||||||
if !abs_path.exists() {
|
if !abs_path.exists() {
|
||||||
tokio::fs::create_dir_all(&abs_path).await
|
tokio::fs::create_dir_all(&abs_path).await
|
||||||
.map_err(|e| StorageMediatorError::AccessError(format!("No se pudo crear el directorio: {}", e)))?;
|
.map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?;
|
||||||
} else if !abs_path.is_dir() {
|
} else if !abs_path.is_dir() {
|
||||||
return Err(StorageMediatorError::InvalidPath(
|
return Err(StorageMediatorError::InvalidPath(
|
||||||
format!("La ruta existe pero no es un directorio: {}", abs_path.display())
|
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -303,17 +303,7 @@ impl TrashUseCase for TrashService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Obtener el elemento de la papelera
|
// Get the trash item
|
||||||
info!("Retrieving trash item from repository: ID={}", trash_id);
|
|
||||||
let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await;
|
|
||||||
|
|
||||||
match item_result {
|
|
||||||
Ok(Some(item)) => {
|
|
||||||
info!("Found item in trash: ID={}, Type={:?}, OriginalID={}",
|
|
||||||
trash_id, item.item_type(), item.original_id());
|
|
||||||
|
|
||||||
// Restore based on type
|
|
||||||
match item.item_type() {
|
|
||||||
TrashedItemType::File => {
|
TrashedItemType::File => {
|
||||||
// Restore the file to its original location
|
// Restore the file to its original location
|
||||||
let file_id = item.original_id().to_string();
|
let file_id = item.original_id().to_string();
|
||||||
@@ -428,7 +418,7 @@ impl TrashUseCase for TrashService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Obtener el elemento de la papelera
|
// Get the trash item
|
||||||
info!("Retrieving trash item from repository: ID={}", trash_id);
|
info!("Retrieving trash item from repository: ID={}", trash_id);
|
||||||
let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await;
|
let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await;
|
||||||
|
|
||||||
@@ -440,7 +430,7 @@ impl TrashUseCase for TrashService {
|
|||||||
// Permanently delete based on type
|
// Permanently delete based on type
|
||||||
match item.item_type() {
|
match item.item_type() {
|
||||||
TrashedItemType::File => {
|
TrashedItemType::File => {
|
||||||
// Eliminar el archivo permanentemente
|
// Permanently delete the file
|
||||||
let file_id = item.original_id().to_string();
|
let file_id = item.original_id().to_string();
|
||||||
|
|
||||||
info!("Permanently deleting file: {}", file_id);
|
info!("Permanently deleting file: {}", file_id);
|
||||||
@@ -466,7 +456,7 @@ impl TrashUseCase for TrashService {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
TrashedItemType::Folder => {
|
TrashedItemType::Folder => {
|
||||||
// Eliminar la carpeta permanentemente
|
// Permanently delete the folder
|
||||||
let folder_id = item.original_id().to_string();
|
let folder_id = item.original_id().to_string();
|
||||||
|
|
||||||
info!("Permanently deleting folder: {}", folder_id);
|
info!("Permanently deleting folder: {}", folder_id);
|
||||||
|
|||||||
@@ -2,22 +2,22 @@ use std::future::Future;
|
|||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use crate::common::errors::{DomainError, ErrorKind};
|
use crate::common::errors::{DomainError, ErrorKind};
|
||||||
|
|
||||||
/// Tipo para operaciones y rollbacks asíncronos
|
/// Type for async operations and rollbacks
|
||||||
type TransactionOp = Pin<Box<dyn Future<Output = Result<(), DomainError>> + Send>>;
|
type TransactionOp = Pin<Box<dyn Future<Output = Result<(), DomainError>> + Send>>;
|
||||||
|
|
||||||
/// Transacción para operaciones de almacenamiento
|
/// Transaction for storage operations
|
||||||
/// Permite definir un conjunto de operaciones y sus rollbacks correspondientes
|
/// Allows defining a set of operations and their corresponding rollbacks
|
||||||
pub struct StorageTransaction {
|
pub struct StorageTransaction {
|
||||||
/// Operaciones a ejecutar
|
/// Operations to execute
|
||||||
operations: Vec<Box<dyn FnOnce() -> TransactionOp + Send>>,
|
operations: Vec<Box<dyn FnOnce() -> TransactionOp + Send>>,
|
||||||
/// Operaciones de rollback para revertir cambios en caso de error
|
/// Rollback operations to revert changes in case of error
|
||||||
rollbacks: Vec<Box<dyn FnOnce() -> TransactionOp + Send>>,
|
rollbacks: Vec<Box<dyn FnOnce() -> TransactionOp + Send>>,
|
||||||
/// Nombre de la transacción para logging
|
/// Transaction name for logging
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StorageTransaction {
|
impl StorageTransaction {
|
||||||
/// Crea una nueva transacción
|
/// Creates a new transaction
|
||||||
pub fn new(name: &str) -> Self {
|
pub fn new(name: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
operations: Vec::new(),
|
operations: Vec::new(),
|
||||||
@@ -26,7 +26,7 @@ impl StorageTransaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade una operación a la transacción con su correspondiente rollback
|
/// Adds an operation to the transaction with its corresponding rollback
|
||||||
pub fn add_operation<F, R>(&mut self, operation: F, rollback: R)
|
pub fn add_operation<F, R>(&mut self, operation: F, rollback: R)
|
||||||
where
|
where
|
||||||
F: Future<Output = Result<(), DomainError>> + Send + 'static,
|
F: Future<Output = Result<(), DomainError>> + Send + 'static,
|
||||||
@@ -36,74 +36,74 @@ impl StorageTransaction {
|
|||||||
self.rollbacks.push(Box::new(move || Box::pin(rollback)));
|
self.rollbacks.push(Box::new(move || Box::pin(rollback)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade una operación sin rollback (para limpieza o logging)
|
/// Adds an operation without rollback (for cleanup or logging)
|
||||||
pub fn add_finalizer<F>(&mut self, finalizer: F)
|
pub fn add_finalizer<F>(&mut self, finalizer: F)
|
||||||
where
|
where
|
||||||
F: Future<Output = Result<(), DomainError>> + Send + 'static,
|
F: Future<Output = Result<(), DomainError>> + Send + 'static,
|
||||||
{
|
{
|
||||||
// El rollback es una operación nula
|
// The rollback is a no-op
|
||||||
let noop = async { Ok(()) };
|
let noop = async { Ok(()) };
|
||||||
|
|
||||||
self.operations.push(Box::new(move || Box::pin(finalizer)));
|
self.operations.push(Box::new(move || Box::pin(finalizer)));
|
||||||
self.rollbacks.push(Box::new(move || Box::pin(noop)));
|
self.rollbacks.push(Box::new(move || Box::pin(noop)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ejecuta la transacción aplicando todas las operaciones en orden
|
/// Executes the transaction by applying all operations in order
|
||||||
/// Si alguna falla, ejecuta los rollbacks en orden inverso
|
/// If any fails, executes rollbacks in reverse order
|
||||||
pub async fn commit(mut self) -> Result<(), DomainError> {
|
pub async fn commit(mut self) -> Result<(), DomainError> {
|
||||||
tracing::debug!("Iniciando transacción: {}", self.name);
|
tracing::debug!("Starting transaction: {}", self.name);
|
||||||
|
|
||||||
let mut completed_ops = Vec::new();
|
let mut completed_ops = Vec::new();
|
||||||
|
|
||||||
// Extraer operaciones para evitar problemas de propiedad
|
// Extract operations to avoid ownership issues
|
||||||
let operations = std::mem::take(&mut self.operations);
|
let operations = std::mem::take(&mut self.operations);
|
||||||
let transaction_name = self.name.clone();
|
let transaction_name = self.name.clone();
|
||||||
|
|
||||||
// Ejecutar operaciones
|
// Execute operations
|
||||||
for (i, op) in operations.into_iter().enumerate() {
|
for (i, op) in operations.into_iter().enumerate() {
|
||||||
match op().await {
|
match op().await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
completed_ops.push(i);
|
completed_ops.push(i);
|
||||||
tracing::trace!("Operación {} completada en transacción: {}", i, transaction_name);
|
tracing::trace!("Operation {} completed in transaction: {}", i, transaction_name);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Error en operación {} de transacción {}: {}", i, transaction_name, e);
|
tracing::error!("Error in operation {} of transaction {}: {}", i, transaction_name, e);
|
||||||
|
|
||||||
// Ejecutar rollbacks para las operaciones completadas en orden inverso
|
// Execute rollbacks for completed operations in reverse order
|
||||||
self.rollback(completed_ops).await?;
|
self.rollback(completed_ops).await?;
|
||||||
|
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::InternalError,
|
ErrorKind::InternalError,
|
||||||
"Transaction",
|
"Transaction",
|
||||||
format!("Falló la transacción '{}': {}", transaction_name, e)
|
format!("Transaction '{}' failed: {}", transaction_name, e)
|
||||||
).with_source(e));
|
).with_source(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("Transacción completada exitosamente: {}", transaction_name);
|
tracing::debug!("Transaction completed successfully: {}", transaction_name);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ejecuta rollbacks para las operaciones completadas
|
/// Executes rollbacks for completed operations
|
||||||
async fn rollback(mut self, completed_ops: Vec<usize>) -> Result<(), DomainError> {
|
async fn rollback(mut self, completed_ops: Vec<usize>) -> Result<(), DomainError> {
|
||||||
tracing::warn!("Iniciando rollback para transacción: {}", self.name);
|
tracing::warn!("Starting rollback for transaction: {}", self.name);
|
||||||
|
|
||||||
let mut rollback_errors = Vec::new();
|
let mut rollback_errors = Vec::new();
|
||||||
|
|
||||||
// Extraer rollbacks para evitar problemas de propiedad
|
// Extract rollbacks to avoid ownership issues
|
||||||
let mut rollbacks = Vec::new();
|
let mut rollbacks = Vec::new();
|
||||||
std::mem::swap(&mut rollbacks, &mut self.rollbacks);
|
std::mem::swap(&mut rollbacks, &mut self.rollbacks);
|
||||||
|
|
||||||
// Ejecutar rollbacks en orden inverso
|
// Execute rollbacks in reverse order
|
||||||
for i in completed_ops.into_iter().rev() {
|
for i in completed_ops.into_iter().rev() {
|
||||||
if i < rollbacks.len() {
|
if i < rollbacks.len() {
|
||||||
// Tomar propiedad del rollback (obtener una referencia mutable)
|
// Take ownership of the rollback (get a mutable reference)
|
||||||
if let Some(rb) = rollbacks.get_mut(i) {
|
if let Some(rb) = rollbacks.get_mut(i) {
|
||||||
// Intercambiar con una función vacía
|
// Swap with an empty function
|
||||||
let rollback = std::mem::replace(rb, Box::new(|| Box::pin(async { Ok(()) })));
|
let rollback = std::mem::replace(rb, Box::new(|| Box::pin(async { Ok(()) })));
|
||||||
if let Err(e) = rollback().await {
|
if let Err(e) = rollback().await {
|
||||||
tracing::error!("Error en rollback de operación {} en transacción {}: {}",
|
tracing::error!("Error in rollback of operation {} in transaction {}: {}",
|
||||||
i, self.name, e);
|
i, self.name, e);
|
||||||
rollback_errors.push(e);
|
rollback_errors.push(e);
|
||||||
}
|
}
|
||||||
@@ -111,20 +111,20 @@ impl StorageTransaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si hubo errores en el rollback, reportarlos
|
// If there were errors during rollback, report them
|
||||||
if !rollback_errors.is_empty() {
|
if !rollback_errors.is_empty() {
|
||||||
tracing::error!("Errores durante rollback de transacción {}: {} errores",
|
tracing::error!("Errors during transaction rollback {}: {} errors",
|
||||||
self.name, rollback_errors.len());
|
self.name, rollback_errors.len());
|
||||||
|
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::InternalError,
|
ErrorKind::InternalError,
|
||||||
"Transaction",
|
"Transaction",
|
||||||
format!("Errores durante rollback de transacción '{}': {} errores",
|
format!("Errors during transaction '{}' rollback: {} errors",
|
||||||
self.name, rollback_errors.len())
|
self.name, rollback_errors.len())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!("Rollback de transacción completado: {}", self.name);
|
tracing::info!("Transaction rollback completed: {}", self.name);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+14
-14
@@ -5,10 +5,10 @@ use std::time::Duration;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Configurar logging
|
// Configure logging
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
// Cargar variables de entorno (primero .env.local, luego .env)
|
// Load environment variables (.env.local first, then .env)
|
||||||
if let Ok(path) = env::var("DOTENV_PATH") {
|
if let Ok(path) = env::var("DOTENV_PATH") {
|
||||||
dotenv::from_path(Path::new(&path)).ok();
|
dotenv::from_path(Path::new(&path)).ok();
|
||||||
} else {
|
} else {
|
||||||
@@ -16,34 +16,34 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtener DATABASE_URL desde variables de entorno
|
// Get DATABASE_URL from environment variables
|
||||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL debe estar configurada");
|
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be configured");
|
||||||
|
|
||||||
println!("Conectando a la base de datos...");
|
println!("Connecting to the database...");
|
||||||
|
|
||||||
// Crear pool de conexiones
|
// Create connection pool
|
||||||
let pool = PgPoolOptions::new()
|
let pool = PgPoolOptions::new()
|
||||||
.max_connections(5)
|
.max_connections(5)
|
||||||
.acquire_timeout(Duration::from_secs(10))
|
.acquire_timeout(Duration::from_secs(10))
|
||||||
.connect(&database_url)
|
.connect(&database_url)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Ejecutar migraciones
|
// Run migrations
|
||||||
println!("Ejecutando migraciones...");
|
println!("Running migrations...");
|
||||||
|
|
||||||
// Obtenemos el directorio desde una variable de entorno o usamos un valor por defecto
|
// Get the directory from an environment variable or use a default value
|
||||||
let migrations_dir = env::var("MIGRATIONS_DIR").unwrap_or_else(|_| "./migrations".to_string());
|
let migrations_dir = env::var("MIGRATIONS_DIR").unwrap_or_else(|_| "./migrations".to_string());
|
||||||
println!("Directorio de migraciones: {}", migrations_dir);
|
println!("Migrations directory: {}", migrations_dir);
|
||||||
|
|
||||||
// Crear un migrator
|
// Create a migrator
|
||||||
let migrator = sqlx::migrate::Migrator::new(Path::new(&migrations_dir))
|
let migrator = sqlx::migrate::Migrator::new(Path::new(&migrations_dir))
|
||||||
.await
|
.await
|
||||||
.expect("No se pudo crear el migrator");
|
.expect("Could not create the migrator");
|
||||||
|
|
||||||
// Ejecutar todas las migraciones pendientes
|
// Run all pending migrations
|
||||||
migrator.run(&pool).await?;
|
migrator.run(&pool).await?;
|
||||||
|
|
||||||
println!("Migraciones aplicadas correctamente");
|
println!("Migrations applied successfully");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
+77
-77
@@ -2,98 +2,98 @@ use std::time::Duration;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
/// Configuración de caché
|
/// Cache configuration
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CacheConfig {
|
pub struct CacheConfig {
|
||||||
/// TTL para entradas de archivos en caché (ms)
|
/// TTL for file cache entries (ms)
|
||||||
pub file_ttl_ms: u64,
|
pub file_ttl_ms: u64,
|
||||||
/// TTL para entradas de directorios en caché (ms)
|
/// TTL for directory cache entries (ms)
|
||||||
pub directory_ttl_ms: u64,
|
pub directory_ttl_ms: u64,
|
||||||
/// Máximo número de entradas en caché
|
/// Maximum number of cache entries
|
||||||
pub max_entries: usize,
|
pub max_entries: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CacheConfig {
|
impl Default for CacheConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
file_ttl_ms: 60_000, // 1 minuto
|
file_ttl_ms: 60_000, // 1 minute
|
||||||
directory_ttl_ms: 120_000, // 2 minutos
|
directory_ttl_ms: 120_000, // 2 minutes
|
||||||
max_entries: 10_000, // 10,000 entradas
|
max_entries: 10_000, // 10,000 entries
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuración de timeouts para diferentes operaciones
|
/// Timeout configuration for different operations
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TimeoutConfig {
|
pub struct TimeoutConfig {
|
||||||
/// Timeout para operaciones de archivo (ms)
|
/// Timeout for file operations (ms)
|
||||||
pub file_operation_ms: u64,
|
pub file_operation_ms: u64,
|
||||||
/// Timeout para operaciones de directorio (ms)
|
/// Timeout for directory operations (ms)
|
||||||
pub dir_operation_ms: u64,
|
pub dir_operation_ms: u64,
|
||||||
/// Timeout para adquisición de locks (ms)
|
/// Timeout for lock acquisition (ms)
|
||||||
pub lock_acquisition_ms: u64,
|
pub lock_acquisition_ms: u64,
|
||||||
/// Timeout para operaciones de red (ms)
|
/// Timeout for network operations (ms)
|
||||||
pub network_operation_ms: u64,
|
pub network_operation_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TimeoutConfig {
|
impl Default for TimeoutConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
file_operation_ms: 10000, // 10 segundos
|
file_operation_ms: 10000, // 10 seconds
|
||||||
dir_operation_ms: 30000, // 30 segundos
|
dir_operation_ms: 30000, // 30 seconds
|
||||||
lock_acquisition_ms: 5000, // 5 segundos
|
lock_acquisition_ms: 5000, // 5 seconds
|
||||||
network_operation_ms: 15000, // 15 segundos
|
network_operation_ms: 15000, // 15 seconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimeoutConfig {
|
impl TimeoutConfig {
|
||||||
/// Obtiene un Duration para operaciones de archivo
|
/// Gets a Duration for file operations
|
||||||
pub fn file_timeout(&self) -> Duration {
|
pub fn file_timeout(&self) -> Duration {
|
||||||
Duration::from_millis(self.file_operation_ms)
|
Duration::from_millis(self.file_operation_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene un Duration para operaciones de escritura de archivo
|
/// Gets a Duration for file write operations
|
||||||
pub fn file_write_timeout(&self) -> Duration {
|
pub fn file_write_timeout(&self) -> Duration {
|
||||||
Duration::from_millis(self.file_operation_ms)
|
Duration::from_millis(self.file_operation_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene un Duration para operaciones de lectura de archivo
|
/// Gets a Duration for file read operations
|
||||||
pub fn file_read_timeout(&self) -> Duration {
|
pub fn file_read_timeout(&self) -> Duration {
|
||||||
Duration::from_millis(self.file_operation_ms)
|
Duration::from_millis(self.file_operation_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene un Duration para operaciones de eliminación de archivo
|
/// Gets a Duration for file delete operations
|
||||||
pub fn file_delete_timeout(&self) -> Duration {
|
pub fn file_delete_timeout(&self) -> Duration {
|
||||||
Duration::from_millis(self.file_operation_ms)
|
Duration::from_millis(self.file_operation_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene un Duration para operaciones de directorio
|
/// Gets a Duration for directory operations
|
||||||
pub fn dir_timeout(&self) -> Duration {
|
pub fn dir_timeout(&self) -> Duration {
|
||||||
Duration::from_millis(self.dir_operation_ms)
|
Duration::from_millis(self.dir_operation_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene un Duration para adquisición de locks
|
/// Gets a Duration for lock acquisition
|
||||||
pub fn lock_timeout(&self) -> Duration {
|
pub fn lock_timeout(&self) -> Duration {
|
||||||
Duration::from_millis(self.lock_acquisition_ms)
|
Duration::from_millis(self.lock_acquisition_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene un Duration para operaciones de red
|
/// Gets a Duration for network operations
|
||||||
pub fn network_timeout(&self) -> Duration {
|
pub fn network_timeout(&self) -> Duration {
|
||||||
Duration::from_millis(self.network_operation_ms)
|
Duration::from_millis(self.network_operation_ms)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuración para manejo de recursos grandes
|
/// Configuration for large resource handling
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ResourceConfig {
|
pub struct ResourceConfig {
|
||||||
/// Umbral en MB para considerar un archivo como grande
|
/// Threshold in MB to consider a file as large
|
||||||
pub large_file_threshold_mb: u64,
|
pub large_file_threshold_mb: u64,
|
||||||
/// Umbral de entradas para considerar un directorio como grande
|
/// Entry threshold to consider a directory as large
|
||||||
pub large_dir_threshold_entries: usize,
|
pub large_dir_threshold_entries: usize,
|
||||||
/// Tamaño de chunk para procesamiento de archivos grandes (bytes)
|
/// Chunk size for large file processing (bytes)
|
||||||
pub chunk_size_bytes: usize,
|
pub chunk_size_bytes: usize,
|
||||||
/// Límite de tamaño de archivo para cargar en memoria (MB)
|
/// File size limit for loading into memory (MB)
|
||||||
pub max_in_memory_file_size_mb: u64,
|
pub max_in_memory_file_size_mb: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ impl Default for ResourceConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
large_file_threshold_mb: 100, // 100 MB
|
large_file_threshold_mb: 100, // 100 MB
|
||||||
large_dir_threshold_entries: 1000, // 1000 entradas
|
large_dir_threshold_entries: 1000, // 1000 entries
|
||||||
chunk_size_bytes: 1024 * 1024, // 1 MB
|
chunk_size_bytes: 1024 * 1024, // 1 MB
|
||||||
max_in_memory_file_size_mb: 50, // 50 MB
|
max_in_memory_file_size_mb: 50, // 50 MB
|
||||||
}
|
}
|
||||||
@@ -109,71 +109,71 @@ impl Default for ResourceConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceConfig {
|
impl ResourceConfig {
|
||||||
/// Convierte un tamaño en bytes a MB
|
/// Converts a size in bytes to MB
|
||||||
pub fn bytes_to_mb(&self, bytes: u64) -> u64 {
|
pub fn bytes_to_mb(&self, bytes: u64) -> u64 {
|
||||||
bytes / (1024 * 1024)
|
bytes / (1024 * 1024)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determina si un archivo es considerado grande
|
/// Determines if a file is considered large
|
||||||
pub fn is_large_file(&self, size_bytes: u64) -> bool {
|
pub fn is_large_file(&self, size_bytes: u64) -> bool {
|
||||||
self.bytes_to_mb(size_bytes) >= self.large_file_threshold_mb
|
self.bytes_to_mb(size_bytes) >= self.large_file_threshold_mb
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determina si un archivo es suficientemente grande para procesamiento paralelo
|
/// Determines if a file is large enough for parallel processing
|
||||||
pub fn needs_parallel_processing(&self, size_bytes: u64, config: &ConcurrencyConfig) -> bool {
|
pub fn needs_parallel_processing(&self, size_bytes: u64, config: &ConcurrencyConfig) -> bool {
|
||||||
self.bytes_to_mb(size_bytes) >= config.min_size_for_parallel_chunks_mb
|
self.bytes_to_mb(size_bytes) >= config.min_size_for_parallel_chunks_mb
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determina si un archivo puede cargarse completo en memoria
|
/// Determines if a file can be fully loaded into memory
|
||||||
pub fn can_load_in_memory(&self, size_bytes: u64) -> bool {
|
pub fn can_load_in_memory(&self, size_bytes: u64) -> bool {
|
||||||
self.bytes_to_mb(size_bytes) <= self.max_in_memory_file_size_mb
|
self.bytes_to_mb(size_bytes) <= self.max_in_memory_file_size_mb
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determina si un directorio es considerado grande
|
/// Determines if a directory is considered large
|
||||||
pub fn is_large_directory(&self, entry_count: usize) -> bool {
|
pub fn is_large_directory(&self, entry_count: usize) -> bool {
|
||||||
entry_count >= self.large_dir_threshold_entries
|
entry_count >= self.large_dir_threshold_entries
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calcula el número de chunks para procesamiento paralelo
|
/// Calculates the number of chunks for parallel processing
|
||||||
pub fn calculate_optimal_chunks(&self, size_bytes: u64, config: &ConcurrencyConfig) -> usize {
|
pub fn calculate_optimal_chunks(&self, size_bytes: u64, config: &ConcurrencyConfig) -> usize {
|
||||||
// Si el archivo no es suficientemente grande, retornar 1
|
// If the file is not large enough, return 1
|
||||||
if !self.needs_parallel_processing(size_bytes, config) {
|
if !self.needs_parallel_processing(size_bytes, config) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calcular el número de chunks basado en el tamaño
|
// Calculate the number of chunks based on size
|
||||||
let chunk_count = (size_bytes as usize + config.parallel_chunk_size_bytes - 1)
|
let chunk_count = (size_bytes as usize + config.parallel_chunk_size_bytes - 1)
|
||||||
/ config.parallel_chunk_size_bytes;
|
/ config.parallel_chunk_size_bytes;
|
||||||
|
|
||||||
// Limitar al máximo de chunks en paralelo
|
// Limit to the maximum number of parallel chunks
|
||||||
chunk_count.min(config.max_parallel_chunks)
|
chunk_count.min(config.max_parallel_chunks)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calcula el tamaño óptimo de cada chunk para procesamiento paralelo
|
/// Calculates the optimal size of each chunk for parallel processing
|
||||||
pub fn calculate_chunk_size(&self, file_size: u64, chunk_count: usize) -> usize {
|
pub fn calculate_chunk_size(&self, file_size: u64, chunk_count: usize) -> usize {
|
||||||
if chunk_count <= 1 {
|
if chunk_count <= 1 {
|
||||||
return file_size as usize;
|
return file_size as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Distribuir equitativamente el tamaño entre los chunks
|
// Distribute the size evenly among the chunks
|
||||||
((file_size as usize) + chunk_count - 1) / chunk_count
|
((file_size as usize) + chunk_count - 1) / chunk_count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuración para operaciones concurrentes
|
/// Configuration for concurrent operations
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ConcurrencyConfig {
|
pub struct ConcurrencyConfig {
|
||||||
/// Máximo de tareas de archivo concurrentes
|
/// Maximum concurrent file tasks
|
||||||
pub max_concurrent_files: usize,
|
pub max_concurrent_files: usize,
|
||||||
/// Máximo de tareas de directorio concurrentes
|
/// Maximum concurrent directory tasks
|
||||||
pub max_concurrent_dirs: usize,
|
pub max_concurrent_dirs: usize,
|
||||||
/// Máximo de operaciones de IO concurrentes
|
/// Maximum concurrent IO operations
|
||||||
pub max_concurrent_io: usize,
|
pub max_concurrent_io: usize,
|
||||||
/// Máximo de chunks para procesar en paralelo por archivo
|
/// Maximum chunks to process in parallel per file
|
||||||
pub max_parallel_chunks: usize,
|
pub max_parallel_chunks: usize,
|
||||||
/// Tamaño mínimo de archivo (MB) para aplicar procesamiento paralelo de chunks
|
/// Minimum file size (MB) to apply parallel chunk processing
|
||||||
pub min_size_for_parallel_chunks_mb: u64,
|
pub min_size_for_parallel_chunks_mb: u64,
|
||||||
/// Tamaño de chunk para procesamiento paralelo (bytes)
|
/// Chunk size for parallel processing (bytes)
|
||||||
pub parallel_chunk_size_bytes: usize,
|
pub parallel_chunk_size_bytes: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,16 +190,16 @@ impl Default for ConcurrencyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuración de almacenamiento
|
/// Storage configuration
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct StorageConfig {
|
pub struct StorageConfig {
|
||||||
/// Directorio raíz para el almacenamiento
|
/// Root directory for storage
|
||||||
pub root_dir: String,
|
pub root_dir: String,
|
||||||
/// Tamaño de chunk para procesamiento de archivos
|
/// Chunk size for file processing
|
||||||
pub chunk_size: usize,
|
pub chunk_size: usize,
|
||||||
/// Umbral para procesamiento paralelo
|
/// Threshold for parallel processing
|
||||||
pub parallel_threshold: usize,
|
pub parallel_threshold: usize,
|
||||||
/// Días de retención para archivos en la papelera
|
/// Retention days for files in the trash
|
||||||
pub trash_retention_days: u32,
|
pub trash_retention_days: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,12 +209,12 @@ impl Default for StorageConfig {
|
|||||||
root_dir: "storage".to_string(),
|
root_dir: "storage".to_string(),
|
||||||
chunk_size: 1024 * 1024, // 1 MB
|
chunk_size: 1024 * 1024, // 1 MB
|
||||||
parallel_threshold: 100 * 1024 * 1024, // 100 MB
|
parallel_threshold: 100 * 1024 * 1024, // 100 MB
|
||||||
trash_retention_days: 30, // 30 días
|
trash_retention_days: 30, // 30 days
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuración de base de datos
|
/// Database configuration
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DatabaseConfig {
|
pub struct DatabaseConfig {
|
||||||
pub connection_string: String,
|
pub connection_string: String,
|
||||||
@@ -239,7 +239,7 @@ impl Default for DatabaseConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuración de autenticación
|
/// Authentication configuration
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AuthConfig {
|
pub struct AuthConfig {
|
||||||
pub jwt_secret: String,
|
pub jwt_secret: String,
|
||||||
@@ -256,15 +256,15 @@ impl Default for AuthConfig {
|
|||||||
// to set OXICLOUD_JWT_SECRET in production. The from_env() method
|
// to set OXICLOUD_JWT_SECRET in production. The from_env() method
|
||||||
// will validate this and warn/panic if not configured.
|
// will validate this and warn/panic if not configured.
|
||||||
jwt_secret: String::new(),
|
jwt_secret: String::new(),
|
||||||
access_token_expiry_secs: 3600, // 1 hora
|
access_token_expiry_secs: 3600, // 1 hour
|
||||||
refresh_token_expiry_secs: 2592000, // 30 días
|
refresh_token_expiry_secs: 2592000, // 30 days
|
||||||
hash_memory_cost: 65536, // 64MB
|
hash_memory_cost: 65536, // 64MB
|
||||||
hash_time_cost: 3,
|
hash_time_cost: 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuración de OpenID Connect (OIDC)
|
/// OpenID Connect (OIDC) configuration
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct OidcConfig {
|
pub struct OidcConfig {
|
||||||
/// Whether OIDC authentication is enabled
|
/// Whether OIDC authentication is enabled
|
||||||
@@ -335,7 +335,7 @@ impl OidcConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuración de funcionalidades (feature flags)
|
/// Feature configuration (feature flags)
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FeaturesConfig {
|
pub struct FeaturesConfig {
|
||||||
pub enable_auth: bool,
|
pub enable_auth: bool,
|
||||||
@@ -357,34 +357,34 @@ impl Default for FeaturesConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuración global de la aplicación
|
/// Global application configuration
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
/// Ruta del directorio de almacenamiento
|
/// Storage directory path
|
||||||
pub storage_path: PathBuf,
|
pub storage_path: PathBuf,
|
||||||
/// Ruta del directorio de archivos estáticos
|
/// Static files directory path
|
||||||
pub static_path: PathBuf,
|
pub static_path: PathBuf,
|
||||||
/// Puerto del servidor
|
/// Server port
|
||||||
pub server_port: u16,
|
pub server_port: u16,
|
||||||
/// Host del servidor
|
/// Server host
|
||||||
pub server_host: String,
|
pub server_host: String,
|
||||||
/// Configuración de caché
|
/// Cache configuration
|
||||||
pub cache: CacheConfig,
|
pub cache: CacheConfig,
|
||||||
/// Configuración de timeouts
|
/// Timeout configuration
|
||||||
pub timeouts: TimeoutConfig,
|
pub timeouts: TimeoutConfig,
|
||||||
/// Configuración de recursos
|
/// Resource configuration
|
||||||
pub resources: ResourceConfig,
|
pub resources: ResourceConfig,
|
||||||
/// Configuración de concurrencia
|
/// Concurrency configuration
|
||||||
pub concurrency: ConcurrencyConfig,
|
pub concurrency: ConcurrencyConfig,
|
||||||
/// Configuración de almacenamiento
|
/// Storage configuration
|
||||||
pub storage: StorageConfig,
|
pub storage: StorageConfig,
|
||||||
/// Configuración de base de datos
|
/// Database configuration
|
||||||
pub database: DatabaseConfig,
|
pub database: DatabaseConfig,
|
||||||
/// Configuración de autenticación
|
/// Authentication configuration
|
||||||
pub auth: AuthConfig,
|
pub auth: AuthConfig,
|
||||||
/// Configuración de funcionalidades
|
/// Feature configuration
|
||||||
pub features: FeaturesConfig,
|
pub features: FeaturesConfig,
|
||||||
/// Configuración OIDC
|
/// OIDC configuration
|
||||||
pub oidc: OidcConfig,
|
pub oidc: OidcConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,7 +412,7 @@ impl AppConfig {
|
|||||||
pub fn from_env() -> Self {
|
pub fn from_env() -> Self {
|
||||||
let mut config = Self::default();
|
let mut config = Self::default();
|
||||||
|
|
||||||
// Usar variables de entorno para sobrescribir valores por defecto
|
// Use environment variables to override default values
|
||||||
if let Ok(storage_path) = env::var("OXICLOUD_STORAGE_PATH") {
|
if let Ok(storage_path) = env::var("OXICLOUD_STORAGE_PATH") {
|
||||||
config.storage_path = PathBuf::from(storage_path);
|
config.storage_path = PathBuf::from(storage_path);
|
||||||
}
|
}
|
||||||
@@ -431,7 +431,7 @@ impl AppConfig {
|
|||||||
config.server_host = server_host;
|
config.server_host = server_host;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuración de Database
|
// Database configuration
|
||||||
if let Ok(connection_string) = env::var("OXICLOUD_DB_CONNECTION_STRING") {
|
if let Ok(connection_string) = env::var("OXICLOUD_DB_CONNECTION_STRING") {
|
||||||
config.database.connection_string = connection_string;
|
config.database.connection_string = connection_string;
|
||||||
}
|
}
|
||||||
@@ -450,7 +450,7 @@ impl AppConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuración Auth
|
// Auth configuration
|
||||||
if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") {
|
if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") {
|
||||||
config.auth.jwt_secret = jwt_secret;
|
config.auth.jwt_secret = jwt_secret;
|
||||||
}
|
}
|
||||||
@@ -582,7 +582,7 @@ impl AppConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtenemos una configuración global por defecto
|
/// Gets a default global configuration
|
||||||
pub fn default_config() -> AppConfig {
|
pub fn default_config() -> AppConfig {
|
||||||
AppConfig::default()
|
AppConfig::default()
|
||||||
}
|
}
|
||||||
+53
-53
@@ -53,10 +53,10 @@ use crate::common::stubs::{
|
|||||||
StubSearchUseCase,
|
StubSearchUseCase,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Fábrica para los diferentes componentes de la aplicación
|
/// Factory for the different application components
|
||||||
///
|
///
|
||||||
/// Esta fábrica centraliza la creación de todos los servicios de la aplicación,
|
/// This factory centralizes the creation of all application services,
|
||||||
/// garantizando el orden correcto de inicialización y resolviendo dependencias circulares.
|
/// ensuring the correct initialization order and resolving circular dependencies.
|
||||||
pub struct AppServiceFactory {
|
pub struct AppServiceFactory {
|
||||||
storage_path: PathBuf,
|
storage_path: PathBuf,
|
||||||
locales_path: PathBuf,
|
locales_path: PathBuf,
|
||||||
@@ -64,7 +64,7 @@ pub struct AppServiceFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AppServiceFactory {
|
impl AppServiceFactory {
|
||||||
/// Crea una nueva fábrica de servicios
|
/// Creates a new service factory
|
||||||
pub fn new(storage_path: PathBuf, locales_path: PathBuf) -> Self {
|
pub fn new(storage_path: PathBuf, locales_path: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
storage_path,
|
storage_path,
|
||||||
@@ -73,7 +73,7 @@ impl AppServiceFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una nueva fábrica de servicios con configuración personalizada
|
/// Creates a new service factory with custom configuration
|
||||||
pub fn with_config(storage_path: PathBuf, locales_path: PathBuf, config: AppConfig) -> Self {
|
pub fn with_config(storage_path: PathBuf, locales_path: PathBuf, config: AppConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
storage_path,
|
storage_path,
|
||||||
@@ -82,17 +82,17 @@ impl AppServiceFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene la configuración
|
/// Gets the configuration
|
||||||
pub fn config(&self) -> &AppConfig {
|
pub fn config(&self) -> &AppConfig {
|
||||||
&self.config
|
&self.config
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene la ruta de almacenamiento
|
/// Gets the storage path
|
||||||
pub fn storage_path(&self) -> &PathBuf {
|
pub fn storage_path(&self) -> &PathBuf {
|
||||||
&self.storage_path
|
&self.storage_path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inicializa los servicios base del sistema
|
/// Initializes the core system services
|
||||||
pub async fn create_core_services(&self) -> Result<CoreServices, DomainError> {
|
pub async fn create_core_services(&self) -> Result<CoreServices, DomainError> {
|
||||||
// Path service
|
// Path service
|
||||||
let path_service = Arc::new(PathService::new(self.storage_path.clone()));
|
let path_service = Arc::new(PathService::new(self.storage_path.clone()));
|
||||||
@@ -105,57 +105,57 @@ impl AppServiceFactory {
|
|||||||
}));
|
}));
|
||||||
tracing::info!("FileContentCache initialized: max 10MB/file, 512MB total, 10k entries");
|
tracing::info!("FileContentCache initialized: max 10MB/file, 512MB total, 10k entries");
|
||||||
|
|
||||||
// ID mapping service para carpetas
|
// ID mapping service for folders
|
||||||
let folder_id_mapping_path = self.storage_path.join("folder_ids.json");
|
let folder_id_mapping_path = self.storage_path.join("folder_ids.json");
|
||||||
let folder_id_mapping_service = Arc::new(
|
let folder_id_mapping_service = Arc::new(
|
||||||
IdMappingService::new(folder_id_mapping_path).await?
|
IdMappingService::new(folder_id_mapping_path).await?
|
||||||
);
|
);
|
||||||
|
|
||||||
// ID mapping service para archivos
|
// ID mapping service for files
|
||||||
let file_id_mapping_path = self.storage_path.join("file_ids.json");
|
let file_id_mapping_path = self.storage_path.join("file_ids.json");
|
||||||
let file_id_mapping_service = Arc::new(
|
let file_id_mapping_service = Arc::new(
|
||||||
IdMappingService::new(file_id_mapping_path).await?
|
IdMappingService::new(file_id_mapping_path).await?
|
||||||
);
|
);
|
||||||
|
|
||||||
// Optimizer con batch processing y caching
|
// Optimizer with batch processing and caching
|
||||||
let id_mapping_optimizer = Arc::new(
|
let id_mapping_optimizer = Arc::new(
|
||||||
IdMappingOptimizer::new(folder_id_mapping_service.clone())
|
IdMappingOptimizer::new(folder_id_mapping_service.clone())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Iniciar tarea de limpieza del optimizer
|
// Start optimizer cleanup task
|
||||||
IdMappingOptimizer::start_cleanup_task(id_mapping_optimizer.clone());
|
IdMappingOptimizer::start_cleanup_task(id_mapping_optimizer.clone());
|
||||||
|
|
||||||
// Thumbnail service para generación de miniaturas
|
// Thumbnail service for thumbnail generation
|
||||||
let thumbnail_service = Arc::new(
|
let thumbnail_service = Arc::new(
|
||||||
crate::infrastructure::services::thumbnail_service::ThumbnailService::new(
|
crate::infrastructure::services::thumbnail_service::ThumbnailService::new(
|
||||||
&self.storage_path,
|
&self.storage_path,
|
||||||
5000, // max 5000 thumbnails en cache
|
5000, // max 5000 thumbnails in cache
|
||||||
100 * 1024 * 1024, // max 100MB de cache
|
100 * 1024 * 1024, // max 100MB cache
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// Inicializar directorios de thumbnails
|
// Initialize thumbnail directories
|
||||||
thumbnail_service.initialize().await?;
|
thumbnail_service.initialize().await?;
|
||||||
|
|
||||||
// Write-behind cache para uploads instantáneos de archivos pequeños
|
// Write-behind cache for instant uploads of small files
|
||||||
let write_behind_cache = crate::infrastructure::services::write_behind_cache::WriteBehindCache::new();
|
let write_behind_cache = crate::infrastructure::services::write_behind_cache::WriteBehindCache::new();
|
||||||
|
|
||||||
// Chunked upload service para archivos grandes (>10MB)
|
// Chunked upload service for large files (>10MB)
|
||||||
let chunked_temp_dir = std::path::PathBuf::from(&self.storage_path).join(".uploads");
|
let chunked_temp_dir = std::path::PathBuf::from(&self.storage_path).join(".uploads");
|
||||||
let chunked_upload_service = Arc::new(
|
let chunked_upload_service = Arc::new(
|
||||||
crate::infrastructure::services::chunked_upload_service::ChunkedUploadService::new(chunked_temp_dir)
|
crate::infrastructure::services::chunked_upload_service::ChunkedUploadService::new(chunked_temp_dir)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Image transcoding service para conversión automática a WebP
|
// Image transcoding service for automatic WebP conversion
|
||||||
let image_transcode_service = Arc::new(
|
let image_transcode_service = Arc::new(
|
||||||
crate::infrastructure::services::image_transcode_service::ImageTranscodeService::new(
|
crate::infrastructure::services::image_transcode_service::ImageTranscodeService::new(
|
||||||
&self.storage_path,
|
&self.storage_path,
|
||||||
2000, // max 2000 imágenes transcodificadas en cache
|
2000, // max 2000 transcoded images in cache
|
||||||
50 * 1024 * 1024, // max 50MB de cache en memoria
|
50 * 1024 * 1024, // max 50MB in-memory cache
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
image_transcode_service.initialize().await?;
|
image_transcode_service.initialize().await?;
|
||||||
|
|
||||||
// Deduplication service para eliminar archivos duplicados
|
// Deduplication service for removing duplicate files
|
||||||
let dedup_service = Arc::new(
|
let dedup_service = Arc::new(
|
||||||
crate::infrastructure::services::dedup_service::DedupService::new(&self.storage_path)
|
crate::infrastructure::services::dedup_service::DedupService::new(&self.storage_path)
|
||||||
);
|
);
|
||||||
@@ -189,7 +189,7 @@ impl AppServiceFactory {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inicializa los servicios de repositorio
|
/// Initializes the repository services
|
||||||
pub fn create_repository_services(&self, core: &CoreServices) -> RepositoryServices {
|
pub fn create_repository_services(&self, core: &CoreServices) -> RepositoryServices {
|
||||||
// Storage mediator - uses stub initially, will be replaced after folder repo is ready
|
// Storage mediator - uses stub initially, will be replaced after folder repo is ready
|
||||||
let storage_mediator_stub: Arc<dyn StorageMediator> = Arc::new(
|
let storage_mediator_stub: Arc<dyn StorageMediator> = Arc::new(
|
||||||
@@ -216,13 +216,13 @@ impl AppServiceFactory {
|
|||||||
FileMetadataCache::default_with_config(core.config.clone())
|
FileMetadataCache::default_with_config(core.config.clone())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Iniciar tarea de limpieza de metadata cache
|
// Start metadata cache cleanup task
|
||||||
let cache_clone = metadata_cache.clone();
|
let cache_clone = metadata_cache.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
FileMetadataCache::start_cleanup_task(cache_clone).await;
|
FileMetadataCache::start_cleanup_task(cache_clone).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Buffer pool para optimización de memoria
|
// Buffer pool for memory optimization
|
||||||
let buffer_pool = BufferPool::new(256 * 1024, 50, 120); // 256KB buffers, 50 max, 2 min TTL
|
let buffer_pool = BufferPool::new(256 * 1024, 50, 120); // 256KB buffers, 50 max, 2 min TTL
|
||||||
BufferPool::start_cleaner(buffer_pool.clone());
|
BufferPool::start_cleaner(buffer_pool.clone());
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ impl AppServiceFactory {
|
|||||||
buffer_pool.clone()
|
buffer_pool.clone()
|
||||||
));
|
));
|
||||||
|
|
||||||
// File repositories separados para lectura y escritura
|
// Separate file repositories for reading and writing
|
||||||
let file_read_repository = Arc::new(FileFsReadRepository::new(
|
let file_read_repository = Arc::new(FileFsReadRepository::new(
|
||||||
self.storage_path.clone(),
|
self.storage_path.clone(),
|
||||||
storage_mediator.clone(),
|
storage_mediator.clone(),
|
||||||
@@ -281,19 +281,19 @@ impl AppServiceFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inicializa los servicios de aplicación
|
/// Initializes the application services
|
||||||
pub fn create_application_services(
|
pub fn create_application_services(
|
||||||
&self,
|
&self,
|
||||||
core: &CoreServices,
|
core: &CoreServices,
|
||||||
repos: &RepositoryServices,
|
repos: &RepositoryServices,
|
||||||
trash_service: Option<Arc<dyn TrashUseCase>>,
|
trash_service: Option<Arc<dyn TrashUseCase>>,
|
||||||
) -> ApplicationServices {
|
) -> ApplicationServices {
|
||||||
// Servicios principales
|
// Main services
|
||||||
let folder_service = Arc::new(FolderService::new(
|
let folder_service = Arc::new(FolderService::new(
|
||||||
repos.folder_repository.clone()
|
repos.folder_repository.clone()
|
||||||
));
|
));
|
||||||
|
|
||||||
// Servicios refactorizados con todos los puertos de infraestructura
|
// Refactored services with all infrastructure ports
|
||||||
let file_upload_service = Arc::new(FileUploadService::new_full(
|
let file_upload_service = Arc::new(FileUploadService::new_full(
|
||||||
repos.file_write_repository.clone(),
|
repos.file_write_repository.clone(),
|
||||||
repos.file_read_repository.clone(),
|
repos.file_read_repository.clone(),
|
||||||
@@ -308,7 +308,7 @@ impl AppServiceFactory {
|
|||||||
core.image_transcode_service.clone(),
|
core.image_transcode_service.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
// FileManagementService con dedup y trash
|
// FileManagementService with dedup and trash
|
||||||
let file_management_service = Arc::new(FileManagementService::new_full(
|
let file_management_service = Arc::new(FileManagementService::new_full(
|
||||||
repos.file_write_repository.clone(),
|
repos.file_write_repository.clone(),
|
||||||
repos.file_read_repository.clone(),
|
repos.file_read_repository.clone(),
|
||||||
@@ -325,7 +325,7 @@ impl AppServiceFactory {
|
|||||||
repos.i18n_repository.clone()
|
repos.i18n_repository.clone()
|
||||||
));
|
));
|
||||||
|
|
||||||
// Search service con caché
|
// Search service with cache
|
||||||
let search_service: Option<Arc<dyn SearchUseCase>> = Some(Arc::new(SearchService::new(
|
let search_service: Option<Arc<dyn SearchUseCase>> = Some(Arc::new(SearchService::new(
|
||||||
repos.file_read_repository.clone(),
|
repos.file_read_repository.clone(),
|
||||||
repos.folder_repository.clone(),
|
repos.folder_repository.clone(),
|
||||||
@@ -336,9 +336,9 @@ impl AppServiceFactory {
|
|||||||
tracing::info!("Application services initialized");
|
tracing::info!("Application services initialized");
|
||||||
|
|
||||||
ApplicationServices {
|
ApplicationServices {
|
||||||
// Tipos concretos para handlers que los necesitan
|
// Concrete types for handlers that need them
|
||||||
folder_service_concrete: folder_service.clone(),
|
folder_service_concrete: folder_service.clone(),
|
||||||
// Traits para abstracción
|
// Traits for abstraction
|
||||||
folder_service,
|
folder_service,
|
||||||
file_upload_service,
|
file_upload_service,
|
||||||
file_retrieval_service,
|
file_retrieval_service,
|
||||||
@@ -347,13 +347,13 @@ impl AppServiceFactory {
|
|||||||
i18n_service,
|
i18n_service,
|
||||||
trash_service, // Already set via parameter
|
trash_service, // Already set via parameter
|
||||||
search_service,
|
search_service,
|
||||||
share_service: None, // Se configura después con create_share_service
|
share_service: None, // Configured later with create_share_service
|
||||||
favorites_service: None, // Se configura después con create_favorites_service
|
favorites_service: None, // Configured later with create_favorites_service
|
||||||
recent_service: None, // Se configura después con create_recent_service
|
recent_service: None, // Configured later with create_recent_service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea el servicio de papelera
|
/// Creates the trash service
|
||||||
pub async fn create_trash_service(
|
pub async fn create_trash_service(
|
||||||
&self,
|
&self,
|
||||||
repos: &RepositoryServices,
|
repos: &RepositoryServices,
|
||||||
@@ -374,7 +374,7 @@ impl AppServiceFactory {
|
|||||||
self.config.storage.trash_retention_days,
|
self.config.storage.trash_retention_days,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Inicializar servicio de limpieza
|
// Initialize cleanup service
|
||||||
let cleanup_service = TrashCleanupService::new(
|
let cleanup_service = TrashCleanupService::new(
|
||||||
service.clone(),
|
service.clone(),
|
||||||
trash_repo.clone(),
|
trash_repo.clone(),
|
||||||
@@ -387,7 +387,7 @@ impl AppServiceFactory {
|
|||||||
Some(service as Arc<dyn TrashUseCase>)
|
Some(service as Arc<dyn TrashUseCase>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea el servicio de compartición
|
/// Creates the sharing service
|
||||||
pub fn create_share_service(
|
pub fn create_share_service(
|
||||||
&self,
|
&self,
|
||||||
repos: &RepositoryServices,
|
repos: &RepositoryServices,
|
||||||
@@ -417,7 +417,7 @@ impl AppServiceFactory {
|
|||||||
Some(service)
|
Some(service)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea el servicio de favoritos (requiere base de datos)
|
/// Creates the favorites service (requires database)
|
||||||
pub fn create_favorites_service(
|
pub fn create_favorites_service(
|
||||||
&self,
|
&self,
|
||||||
db_pool: &Arc<PgPool>,
|
db_pool: &Arc<PgPool>,
|
||||||
@@ -430,7 +430,7 @@ impl AppServiceFactory {
|
|||||||
service
|
service
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea el servicio de elementos recientes (requiere base de datos)
|
/// Creates the recent items service (requires database)
|
||||||
pub fn create_recent_service(
|
pub fn create_recent_service(
|
||||||
&self,
|
&self,
|
||||||
db_pool: &Arc<PgPool>,
|
db_pool: &Arc<PgPool>,
|
||||||
@@ -446,7 +446,7 @@ impl AppServiceFactory {
|
|||||||
service
|
service
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Precarga traducciones
|
/// Preloads translations
|
||||||
pub async fn preload_translations(&self, i18n_service: &I18nApplicationService) {
|
pub async fn preload_translations(&self, i18n_service: &I18nApplicationService) {
|
||||||
use crate::domain::services::i18n_service::Locale;
|
use crate::domain::services::i18n_service::Locale;
|
||||||
|
|
||||||
@@ -468,7 +468,7 @@ impl AppServiceFactory {
|
|||||||
tracing::info!("Translations preloaded");
|
tracing::info!("Translations preloaded");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Precarga directorios en caché
|
/// Preloads directories into cache
|
||||||
pub async fn preload_cache(&self, metadata_cache: &FileMetadataCache) {
|
pub async fn preload_cache(&self, metadata_cache: &FileMetadataCache) {
|
||||||
tracing::info!("Preloading common directories to warm up cache...");
|
tracing::info!("Preloading common directories to warm up cache...");
|
||||||
if let Ok(count) = metadata_cache.preload_directory(&self.storage_path, true, 1).await {
|
if let Ok(count) = metadata_cache.preload_directory(&self.storage_path, true, 1).await {
|
||||||
@@ -476,7 +476,7 @@ impl AppServiceFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea el servicio de uso de almacenamiento (requiere base de datos)
|
/// Creates the storage usage service (requires database)
|
||||||
pub fn create_storage_usage_service(
|
pub fn create_storage_usage_service(
|
||||||
&self,
|
&self,
|
||||||
repos: &RepositoryServices,
|
repos: &RepositoryServices,
|
||||||
@@ -495,9 +495,9 @@ impl AppServiceFactory {
|
|||||||
service
|
service
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construye el AppState completo usando todos los servicios de la fábrica.
|
/// Builds the complete AppState using all factory services.
|
||||||
///
|
///
|
||||||
/// Este es el punto de entrada principal que reemplaza toda la lógica manual de `main.rs`.
|
/// This is the main entry point that replaces all manual logic in `main.rs`.
|
||||||
pub async fn build_app_state(
|
pub async fn build_app_state(
|
||||||
&self,
|
&self,
|
||||||
db_pool: Option<Arc<PgPool>>,
|
db_pool: Option<Arc<PgPool>>,
|
||||||
@@ -677,7 +677,7 @@ impl AppServiceFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contenedor para servicios base
|
/// Container for core services
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CoreServices {
|
pub struct CoreServices {
|
||||||
pub path_service: Arc<PathService>,
|
pub path_service: Arc<PathService>,
|
||||||
@@ -695,7 +695,7 @@ pub struct CoreServices {
|
|||||||
pub config: AppConfig,
|
pub config: AppConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contenedor para servicios de repositorio
|
/// Container for repository services
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RepositoryServices {
|
pub struct RepositoryServices {
|
||||||
pub folder_repository: Arc<dyn FolderStoragePort>,
|
pub folder_repository: Arc<dyn FolderStoragePort>,
|
||||||
@@ -707,12 +707,12 @@ pub struct RepositoryServices {
|
|||||||
pub trash_repository: Option<Arc<dyn crate::domain::repositories::trash_repository::TrashRepository>>,
|
pub trash_repository: Option<Arc<dyn crate::domain::repositories::trash_repository::TrashRepository>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contenedor para servicios de aplicación
|
/// Container for application services
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ApplicationServices {
|
pub struct ApplicationServices {
|
||||||
// Tipos concretos para compatibilidad con handlers existentes
|
// Concrete types for compatibility with existing handlers
|
||||||
pub folder_service_concrete: Arc<FolderService>,
|
pub folder_service_concrete: Arc<FolderService>,
|
||||||
// Traits para abstracción
|
// Traits for abstraction
|
||||||
pub folder_service: Arc<dyn FolderUseCase>,
|
pub folder_service: Arc<dyn FolderUseCase>,
|
||||||
pub file_upload_service: Arc<dyn FileUploadUseCase>,
|
pub file_upload_service: Arc<dyn FileUploadUseCase>,
|
||||||
pub file_retrieval_service: Arc<dyn FileRetrievalUseCase>,
|
pub file_retrieval_service: Arc<dyn FileRetrievalUseCase>,
|
||||||
@@ -726,14 +726,14 @@ pub struct ApplicationServices {
|
|||||||
pub recent_service: Option<Arc<dyn RecentItemsUseCase>>,
|
pub recent_service: Option<Arc<dyn RecentItemsUseCase>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contenedor para servicios de autenticación
|
/// Container for authentication services
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AuthServices {
|
pub struct AuthServices {
|
||||||
pub token_service: Arc<dyn crate::application::ports::auth_ports::TokenServicePort>,
|
pub token_service: Arc<dyn crate::application::ports::auth_ports::TokenServicePort>,
|
||||||
pub auth_application_service: Arc<AuthApplicationService>,
|
pub auth_application_service: Arc<AuthApplicationService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Estado global de la aplicación para dependency injection
|
/// Global application state for dependency injection
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub core: CoreServices,
|
pub core: CoreServices,
|
||||||
|
|||||||
+14
-14
@@ -1,21 +1,21 @@
|
|||||||
//! Errores de la aplicación
|
//! Application errors
|
||||||
//!
|
//!
|
||||||
//! Este módulo re-exporta los errores del dominio para compatibilidad.
|
//! This module re-exports domain errors for compatibility.
|
||||||
//! Las conversiones de errores de infraestructura (sqlx, serde_json, etc.)
|
//! Infrastructure error conversions (sqlx, serde_json, etc.)
|
||||||
//! se encuentran en infrastructure/adapters/error_adapters.rs, siguiendo
|
//! are located in infrastructure/adapters/error_adapters.rs, following
|
||||||
//! los principios de Clean Architecture donde el dominio no debe conocer
|
//! Clean Architecture principles where the domain should not know about
|
||||||
//! detalles de infraestructura.
|
//! infrastructure details.
|
||||||
|
|
||||||
// Re-exportar errores del dominio para compatibilidad
|
// Re-export domain errors for compatibility
|
||||||
pub use crate::domain::errors::{DomainError, ErrorKind, Result};
|
pub use crate::domain::errors::{DomainError, ErrorKind, Result};
|
||||||
|
|
||||||
// Re-exportar AppError desde interfaces para compatibilidad hacia atrás
|
// Re-export AppError from interfaces for backward compatibility
|
||||||
// NOTA: El lugar canónico de AppError es ahora crate::interfaces::errors
|
// NOTE: The canonical location of AppError is now crate::interfaces::errors
|
||||||
|
|
||||||
// Las conversiones de errores de infraestructura se han movido a:
|
// Infrastructure error conversions have been moved to:
|
||||||
// crate::infrastructure::adapters::error_adapters
|
// crate::infrastructure::adapters::error_adapters
|
||||||
//
|
//
|
||||||
// Para convertir errores de infraestructura a DomainError, use:
|
// To convert infrastructure errors to DomainError, use:
|
||||||
// - El trait IntoDomainError para conversiones explícitas con contexto
|
// - The IntoDomainError trait for explicit conversions with context
|
||||||
// - O maneje los errores en los repositorios/servicios de infraestructura
|
// - Or handle errors in infrastructure repositories/services
|
||||||
// usando map_err() con DomainError::internal_error() o métodos similares
|
// using map_err() with DomainError::internal_error() or similar methods
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use chrono::{DateTime, Utc};
|
|||||||
|
|
||||||
use crate::common::errors::{Result, DomainError, ErrorKind};
|
use crate::common::errors::{Result, DomainError, ErrorKind};
|
||||||
|
|
||||||
// Re-exportar errores de entidad desde el módulo centralizado
|
// Re-export entity errors from the centralized module
|
||||||
pub use super::entity_errors::CalendarError;
|
pub use super::entity_errors::CalendarError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use chrono::{DateTime, Utc, Duration, TimeZone};
|
|||||||
|
|
||||||
use crate::common::errors::{Result, DomainError, ErrorKind};
|
use crate::common::errors::{Result, DomainError, ErrorKind};
|
||||||
|
|
||||||
// Re-exportar errores de entidad desde el módulo centralizado
|
// Re-export entity errors from the centralized module
|
||||||
pub use super::entity_errors::CalendarEventError;
|
pub use super::entity_errors::CalendarEventError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
//! Errores puros de entidades de dominio
|
//! Pure domain entity errors
|
||||||
//!
|
//!
|
||||||
//! Este módulo define los errores específicos de las entidades de dominio
|
//! This module defines domain entity-specific errors
|
||||||
//! sin dependencias de frameworks externos, siguiendo los principios de
|
//! without external framework dependencies, following
|
||||||
//! Clean Architecture.
|
//! Clean Architecture principles.
|
||||||
//!
|
//!
|
||||||
//! Los errores implementan manualmente `std::error::Error` y `std::fmt::Display`
|
//! Errors manually implement `std::error::Error` and `std::fmt::Display`
|
||||||
//! para mantener el dominio libre de dependencias externas.
|
//! to keep the domain free of external dependencies.
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||||
@@ -14,12 +14,12 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
|
|||||||
// FILE ERRORS
|
// FILE ERRORS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Errores que pueden ocurrir durante operaciones con entidades File
|
/// Errors that can occur during File entity operations
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum FileError {
|
pub enum FileError {
|
||||||
/// Ocurre cuando el nombre de archivo contiene caracteres inválidos o está vacío
|
/// Occurs when the file name contains invalid characters or is empty
|
||||||
InvalidFileName(String),
|
InvalidFileName(String),
|
||||||
/// Ocurre cuando falla la validación de cualquier atributo de la entidad
|
/// Occurs when validation of any entity attribute fails
|
||||||
ValidationError(String),
|
ValidationError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,19 +34,19 @@ impl Display for FileError {
|
|||||||
|
|
||||||
impl Error for FileError {}
|
impl Error for FileError {}
|
||||||
|
|
||||||
/// Alias de tipo para resultados de operaciones con entidades File
|
/// Type alias for File entity operation results
|
||||||
pub type FileResult<T> = Result<T, FileError>;
|
pub type FileResult<T> = Result<T, FileError>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// FOLDER ERRORS
|
// FOLDER ERRORS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Errores que pueden ocurrir durante operaciones con entidades Folder
|
/// Errors that can occur during Folder entity operations
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum FolderError {
|
pub enum FolderError {
|
||||||
/// Ocurre cuando el nombre de carpeta contiene caracteres inválidos o está vacío
|
/// Occurs when the folder name contains invalid characters or is empty
|
||||||
InvalidFolderName(String),
|
InvalidFolderName(String),
|
||||||
/// Ocurre cuando falla la validación de cualquier atributo de la entidad
|
/// Occurs when validation of any entity attribute fails
|
||||||
ValidationError(String),
|
ValidationError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,54 +61,54 @@ impl Display for FolderError {
|
|||||||
|
|
||||||
impl Error for FolderError {}
|
impl Error for FolderError {}
|
||||||
|
|
||||||
/// Alias de tipo para resultados de operaciones con entidades Folder
|
/// Type alias for Folder entity operation results
|
||||||
pub type FolderResult<T> = Result<T, FolderError>;
|
pub type FolderResult<T> = Result<T, FolderError>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// USER ERRORS
|
// USER ERRORS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Errores que pueden ocurrir durante operaciones con entidades User
|
/// Errors that can occur during User entity operations
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum UserError {
|
pub enum UserError {
|
||||||
/// Nombre de usuario inválido
|
/// Invalid username
|
||||||
InvalidUsername(String),
|
InvalidUsername(String),
|
||||||
/// Contraseña inválida
|
/// Invalid password
|
||||||
InvalidPassword(String),
|
InvalidPassword(String),
|
||||||
/// Error de validación general
|
/// General validation error
|
||||||
ValidationError(String),
|
ValidationError(String),
|
||||||
/// Error de autenticación
|
/// Authentication error
|
||||||
AuthenticationError(String),
|
AuthenticationError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for UserError {
|
impl Display for UserError {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
match self {
|
match self {
|
||||||
UserError::InvalidUsername(msg) => write!(f, "Username inválido: {}", msg),
|
UserError::InvalidUsername(msg) => write!(f, "Invalid username: {}", msg),
|
||||||
UserError::InvalidPassword(msg) => write!(f, "Password inválido: {}", msg),
|
UserError::InvalidPassword(msg) => write!(f, "Invalid password: {}", msg),
|
||||||
UserError::ValidationError(msg) => write!(f, "Error en la validación: {}", msg),
|
UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
|
||||||
UserError::AuthenticationError(msg) => write!(f, "Error en la autenticación: {}", msg),
|
UserError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error for UserError {}
|
impl Error for UserError {}
|
||||||
|
|
||||||
/// Alias de tipo para resultados de operaciones con entidades User
|
/// Type alias for User entity operation results
|
||||||
pub type UserResult<T> = Result<T, UserError>;
|
pub type UserResult<T> = Result<T, UserError>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// SHARE ERRORS
|
// SHARE ERRORS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Errores que pueden ocurrir durante operaciones con entidades Share
|
/// Errors that can occur during Share entity operations
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum ShareError {
|
pub enum ShareError {
|
||||||
/// Token de compartición inválido
|
/// Invalid share token
|
||||||
InvalidToken(String),
|
InvalidToken(String),
|
||||||
/// Fecha de expiración inválida
|
/// Invalid expiration date
|
||||||
InvalidExpiration(String),
|
InvalidExpiration(String),
|
||||||
/// Error de validación general
|
/// General validation error
|
||||||
ValidationError(String),
|
ValidationError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,21 +124,21 @@ impl Display for ShareError {
|
|||||||
|
|
||||||
impl Error for ShareError {}
|
impl Error for ShareError {}
|
||||||
|
|
||||||
/// Alias de tipo para resultados de operaciones con entidades Share
|
/// Type alias for Share entity operation results
|
||||||
pub type ShareResult<T> = Result<T, ShareError>;
|
pub type ShareResult<T> = Result<T, ShareError>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CALENDAR ERRORS
|
// CALENDAR ERRORS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Errores que pueden ocurrir durante operaciones con entidades Calendar
|
/// Errors that can occur during Calendar entity operations
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum CalendarError {
|
pub enum CalendarError {
|
||||||
/// Nombre de calendario inválido
|
/// Invalid calendar name
|
||||||
InvalidName(String),
|
InvalidName(String),
|
||||||
/// Código de color inválido
|
/// Invalid color code
|
||||||
InvalidColor(String),
|
InvalidColor(String),
|
||||||
/// ID de propietario inválido
|
/// Invalid owner ID
|
||||||
InvalidOwnerId(String),
|
InvalidOwnerId(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,23 +154,23 @@ impl Display for CalendarError {
|
|||||||
|
|
||||||
impl Error for CalendarError {}
|
impl Error for CalendarError {}
|
||||||
|
|
||||||
/// Alias de tipo para resultados de operaciones con entidades Calendar
|
/// Type alias for Calendar entity operation results
|
||||||
pub type CalendarResult<T> = Result<T, CalendarError>;
|
pub type CalendarResult<T> = Result<T, CalendarError>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CALENDAR EVENT ERRORS
|
// CALENDAR EVENT ERRORS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Errores que pueden ocurrir durante operaciones con entidades CalendarEvent
|
/// Errors that can occur during CalendarEvent entity operations
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum CalendarEventError {
|
pub enum CalendarEventError {
|
||||||
/// Resumen/título de evento inválido
|
/// Invalid event summary/title
|
||||||
InvalidSummary(String),
|
InvalidSummary(String),
|
||||||
/// Fechas de evento inválidas
|
/// Invalid event dates
|
||||||
InvalidDates(String),
|
InvalidDates(String),
|
||||||
/// Regla de recurrencia inválida
|
/// Invalid recurrence rule
|
||||||
InvalidRecurrence(String),
|
InvalidRecurrence(String),
|
||||||
/// Datos iCalendar inválidos
|
/// Invalid iCalendar data
|
||||||
InvalidICalData(String),
|
InvalidICalData(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ impl Display for CalendarEventError {
|
|||||||
|
|
||||||
impl Error for CalendarEventError {}
|
impl Error for CalendarEventError {}
|
||||||
|
|
||||||
/// Alias de tipo para resultados de operaciones con entidades CalendarEvent
|
/// Type alias for CalendarEvent entity operation results
|
||||||
pub type CalendarEventResult<T> = Result<T, CalendarEventError>;
|
pub type CalendarEventResult<T> = Result<T, CalendarEventError>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -216,10 +216,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_user_error_display() {
|
fn test_user_error_display() {
|
||||||
let err = UserError::InvalidUsername("".to_string());
|
let err = UserError::InvalidUsername("".to_string());
|
||||||
assert_eq!(err.to_string(), "Username inválido: ");
|
assert_eq!(err.to_string(), "Invalid username: ");
|
||||||
|
|
||||||
let err = UserError::AuthenticationError("invalid credentials".to_string());
|
let err = UserError::AuthenticationError("invalid credentials".to_string());
|
||||||
assert_eq!(err.to_string(), "Error en la autenticación: invalid credentials");
|
assert_eq!(err.to_string(), "Authentication error: invalid credentials");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::StoragePath;
|
||||||
|
|
||||||
// Re-exportar errores de entidad desde el módulo centralizado
|
// Re-export entity errors from the centralized module
|
||||||
pub use super::entity_errors::{FileError, FileResult};
|
pub use super::entity_errors::{FileError, FileResult};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -366,6 +366,6 @@ mod tests {
|
|||||||
assert!(renamed.is_ok());
|
assert!(renamed.is_ok());
|
||||||
let renamed = renamed.unwrap();
|
let renamed = renamed.unwrap();
|
||||||
assert_eq!(renamed.name(), "newname.txt");
|
assert_eq!(renamed.name(), "newname.txt");
|
||||||
assert_eq!(renamed.id(), "123"); // El ID no cambia
|
assert_eq!(renamed.id(), "123"); // The ID does not change
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::StoragePath;
|
||||||
|
|
||||||
// Re-exportar errores de entidad desde el módulo centralizado
|
// Re-export entity errors from the centralized module
|
||||||
pub use super::entity_errors::{FolderError, FolderResult};
|
pub use super::entity_errors::{FolderError, FolderResult};
|
||||||
|
|
||||||
/// Represents a folder entity in the domain
|
/// Represents a folder entity in the domain
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// Re-exportar errores de entidad desde el módulo centralizado
|
// Re-export entity errors from the centralized module
|
||||||
pub use super::entity_errors::ShareError;
|
pub use super::entity_errors::ShareError;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
|||||||
+12
-12
@@ -1,7 +1,7 @@
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
// Re-exportar errores de entidad desde el módulo centralizado
|
// Re-export entity errors from the centralized module
|
||||||
pub use super::entity_errors::{UserError, UserResult};
|
pub use super::entity_errors::{UserError, UserResult};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -57,22 +57,22 @@ impl User {
|
|||||||
role: UserRole,
|
role: UserRole,
|
||||||
storage_quota_bytes: i64,
|
storage_quota_bytes: i64,
|
||||||
) -> UserResult<Self> {
|
) -> UserResult<Self> {
|
||||||
// Validaciones
|
// Validations
|
||||||
if username.is_empty() || username.len() < 3 || username.len() > 32 {
|
if username.is_empty() || username.len() < 3 || username.len() > 32 {
|
||||||
return Err(UserError::InvalidUsername(format!(
|
return Err(UserError::InvalidUsername(format!(
|
||||||
"Username debe tener entre 3 y 32 caracteres"
|
"Username must be between 3 and 32 characters"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !email.contains('@') || email.len() < 5 {
|
if !email.contains('@') || email.len() < 5 {
|
||||||
return Err(UserError::ValidationError(format!(
|
return Err(UserError::ValidationError(format!(
|
||||||
"Email inválido"
|
"Invalid email"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if password_hash.is_empty() {
|
if password_hash.is_empty() {
|
||||||
return Err(UserError::InvalidPassword(format!(
|
return Err(UserError::InvalidPassword(format!(
|
||||||
"Password hash no puede estar vacío"
|
"Password hash cannot be empty"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,12 +106,12 @@ impl User {
|
|||||||
) -> UserResult<Self> {
|
) -> UserResult<Self> {
|
||||||
if username.is_empty() || username.len() < 3 || username.len() > 32 {
|
if username.is_empty() || username.len() < 3 || username.len() > 32 {
|
||||||
return Err(UserError::InvalidUsername(
|
return Err(UserError::InvalidUsername(
|
||||||
"Username debe tener entre 3 y 32 caracteres".to_string(),
|
"Username must be between 3 and 32 characters".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !email.contains('@') || email.len() < 5 {
|
if !email.contains('@') || email.len() < 5 {
|
||||||
return Err(UserError::ValidationError(
|
return Err(UserError::ValidationError(
|
||||||
"Email inválido".to_string(),
|
"Invalid email".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
@@ -132,7 +132,7 @@ impl User {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crear desde valores existentes (para reconstrucción desde BD)
|
// Create from existing values (for reconstruction from DB)
|
||||||
pub fn from_data(
|
pub fn from_data(
|
||||||
id: String,
|
id: String,
|
||||||
username: String,
|
username: String,
|
||||||
@@ -263,26 +263,26 @@ impl User {
|
|||||||
self.updated_at = Utc::now();
|
self.updated_at = Utc::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar uso de almacenamiento
|
// Update storage usage
|
||||||
pub fn update_storage_used(&mut self, storage_used_bytes: i64) {
|
pub fn update_storage_used(&mut self, storage_used_bytes: i64) {
|
||||||
self.storage_used_bytes = storage_used_bytes;
|
self.storage_used_bytes = storage_used_bytes;
|
||||||
self.updated_at = Utc::now();
|
self.updated_at = Utc::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registrar login
|
// Register login
|
||||||
pub fn register_login(&mut self) {
|
pub fn register_login(&mut self) {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
self.last_login_at = Some(now);
|
self.last_login_at = Some(now);
|
||||||
self.updated_at = now;
|
self.updated_at = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Desactivar usuario
|
// Deactivate user
|
||||||
pub fn deactivate(&mut self) {
|
pub fn deactivate(&mut self) {
|
||||||
self.active = false;
|
self.active = false;
|
||||||
self.updated_at = Utc::now();
|
self.updated_at = Utc::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activar usuario
|
// Activate user
|
||||||
pub fn activate(&mut self) {
|
pub fn activate(&mut self) {
|
||||||
self.active = true;
|
self.active = true;
|
||||||
self.updated_at = Utc::now();
|
self.updated_at = Utc::now();
|
||||||
|
|||||||
+34
-34
@@ -1,35 +1,35 @@
|
|||||||
//! Errores del dominio
|
//! Domain errors
|
||||||
//!
|
//!
|
||||||
//! Este módulo contiene los tipos de error propios del dominio.
|
//! This module contains domain-specific error types.
|
||||||
//! DomainError es el error base que se usa en toda la capa de dominio.
|
//! DomainError is the base error used throughout the domain layer.
|
||||||
|
|
||||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||||
use std::error::Error as StdError;
|
use std::error::Error as StdError;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Tipo Result común para el dominio con DomainError como error estándar
|
/// Common Result type for the domain with DomainError as the standard error
|
||||||
pub type Result<T> = std::result::Result<T, DomainError>;
|
pub type Result<T> = std::result::Result<T, DomainError>;
|
||||||
|
|
||||||
/// Tipos de errores del dominio
|
/// Domain error types
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ErrorKind {
|
pub enum ErrorKind {
|
||||||
/// Entidad no encontrada
|
/// Entity not found
|
||||||
NotFound,
|
NotFound,
|
||||||
/// Entidad ya existe
|
/// Entity already exists
|
||||||
AlreadyExists,
|
AlreadyExists,
|
||||||
/// Entrada inválida o validación fallida
|
/// Invalid input or failed validation
|
||||||
InvalidInput,
|
InvalidInput,
|
||||||
/// Error de acceso o permisos
|
/// Access or permissions error
|
||||||
AccessDenied,
|
AccessDenied,
|
||||||
/// Tiempo de espera agotado
|
/// Timeout expired
|
||||||
Timeout,
|
Timeout,
|
||||||
/// Error interno del sistema
|
/// Internal system error
|
||||||
InternalError,
|
InternalError,
|
||||||
/// Funcionalidad no implementada
|
/// Functionality not implemented
|
||||||
NotImplemented,
|
NotImplemented,
|
||||||
/// Operación no soportada
|
/// Unsupported operation
|
||||||
UnsupportedOperation,
|
UnsupportedOperation,
|
||||||
/// Error de base de datos
|
/// Database error
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,25 +49,25 @@ impl Display for ErrorKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error base de dominio que proporciona contexto detallado
|
/// Base domain error that provides detailed context
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
#[error("{kind}: {message}")]
|
#[error("{kind}: {message}")]
|
||||||
pub struct DomainError {
|
pub struct DomainError {
|
||||||
/// Tipo de error
|
/// Error type
|
||||||
pub kind: ErrorKind,
|
pub kind: ErrorKind,
|
||||||
/// Tipo de entidad afectada (ej: "File", "Folder")
|
/// Affected entity type (e.g.: "File", "Folder")
|
||||||
pub entity_type: &'static str,
|
pub entity_type: &'static str,
|
||||||
/// Identificador de la entidad si está disponible
|
/// Entity identifier if available
|
||||||
pub entity_id: Option<String>,
|
pub entity_id: Option<String>,
|
||||||
/// Mensaje descriptivo del error
|
/// Descriptive error message
|
||||||
pub message: String,
|
pub message: String,
|
||||||
/// Error fuente (opcional)
|
/// Source error (optional)
|
||||||
#[source]
|
#[source]
|
||||||
pub source: Option<Box<dyn StdError + Send + Sync>>,
|
pub source: Option<Box<dyn StdError + Send + Sync>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DomainError {
|
impl DomainError {
|
||||||
/// Crea un nuevo error de dominio
|
/// Creates a new domain error
|
||||||
pub fn new<S: Into<String>>(
|
pub fn new<S: Into<String>>(
|
||||||
kind: ErrorKind,
|
kind: ErrorKind,
|
||||||
entity_type: &'static str,
|
entity_type: &'static str,
|
||||||
@@ -82,7 +82,7 @@ impl DomainError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un error de entidad no encontrada
|
/// Creates an entity not found error
|
||||||
pub fn not_found<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
|
pub fn not_found<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
|
||||||
let id = entity_id.into();
|
let id = entity_id.into();
|
||||||
Self {
|
Self {
|
||||||
@@ -94,7 +94,7 @@ impl DomainError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un error de entidad ya existente
|
/// Creates an entity already exists error
|
||||||
pub fn already_exists<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
|
pub fn already_exists<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
|
||||||
let id = entity_id.into();
|
let id = entity_id.into();
|
||||||
Self {
|
Self {
|
||||||
@@ -106,7 +106,7 @@ impl DomainError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un error para operaciones no soportadas
|
/// Creates an error for unsupported operations
|
||||||
pub fn operation_not_supported<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
pub fn operation_not_supported<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||||
Self::new(
|
Self::new(
|
||||||
ErrorKind::UnsupportedOperation,
|
ErrorKind::UnsupportedOperation,
|
||||||
@@ -115,7 +115,7 @@ impl DomainError {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un error de tiempo agotado
|
/// Creates a timeout error
|
||||||
pub fn timeout<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
pub fn timeout<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
kind: ErrorKind::Timeout,
|
kind: ErrorKind::Timeout,
|
||||||
@@ -126,7 +126,7 @@ impl DomainError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un error interno
|
/// Creates an internal error
|
||||||
pub fn internal_error<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
pub fn internal_error<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
kind: ErrorKind::InternalError,
|
kind: ErrorKind::InternalError,
|
||||||
@@ -137,7 +137,7 @@ impl DomainError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un error de acceso denegado
|
/// Creates an access denied error
|
||||||
pub fn access_denied<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
pub fn access_denied<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
kind: ErrorKind::AccessDenied,
|
kind: ErrorKind::AccessDenied,
|
||||||
@@ -159,7 +159,7 @@ impl DomainError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un error de base de datos
|
/// Creates a database error
|
||||||
pub fn database_error<S: Into<String>>(message: S) -> Self {
|
pub fn database_error<S: Into<String>>(message: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
kind: ErrorKind::DatabaseError,
|
kind: ErrorKind::DatabaseError,
|
||||||
@@ -170,7 +170,7 @@ impl DomainError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un error de validación
|
/// Creates a validation error
|
||||||
pub fn validation_error<S: Into<String>>(message: S) -> Self {
|
pub fn validation_error<S: Into<String>>(message: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
kind: ErrorKind::InvalidInput,
|
kind: ErrorKind::InvalidInput,
|
||||||
@@ -181,7 +181,7 @@ impl DomainError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un error de funcionalidad no implementada
|
/// Creates a not implemented error
|
||||||
pub fn not_implemented<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
pub fn not_implemented<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
kind: ErrorKind::NotImplemented,
|
kind: ErrorKind::NotImplemented,
|
||||||
@@ -192,20 +192,20 @@ impl DomainError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece el ID de la entidad
|
/// Sets the entity ID
|
||||||
pub fn with_id<S: Into<String>>(mut self, entity_id: S) -> Self {
|
pub fn with_id<S: Into<String>>(mut self, entity_id: S) -> Self {
|
||||||
self.entity_id = Some(entity_id.into());
|
self.entity_id = Some(entity_id.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece el error fuente
|
/// Sets the source error
|
||||||
pub fn with_source<E: StdError + Send + Sync + 'static>(mut self, source: E) -> Self {
|
pub fn with_source<E: StdError + Send + Sync + 'static>(mut self, source: E) -> Self {
|
||||||
self.source = Some(Box::new(source));
|
self.source = Some(Box::new(source));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait para añadir contexto a los errores
|
/// Trait for adding context to errors
|
||||||
pub trait ErrorContext<T, E> {
|
pub trait ErrorContext<T, E> {
|
||||||
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
|
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
|
||||||
where
|
where
|
||||||
@@ -245,7 +245,7 @@ impl<T, E: StdError + Send + Sync + 'static> ErrorContext<T, E> for std::result:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementaciones From para errores estándar (sin dependencias externas de infra)
|
// From implementations for standard errors (without external infrastructure dependencies)
|
||||||
impl From<std::io::Error> for DomainError {
|
impl From<std::io::Error> for DomainError {
|
||||||
fn from(err: std::io::Error) -> Self {
|
fn from(err: std::io::Error) -> Self {
|
||||||
DomainError {
|
DomainError {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
//! Puerto de persistencia del dominio para la entidad File.
|
//! Domain persistence port for the File entity.
|
||||||
//!
|
//!
|
||||||
//! Define el contrato que cualquier implementación de almacenamiento de archivos
|
//! Defines the contract that any file storage implementation must fulfill.
|
||||||
//! debe cumplir. Este trait vive en el dominio porque File es una entidad core
|
//! This trait lives in the domain because File is a core entity of the system
|
||||||
//! del sistema y sus contratos de persistencia pertenecen a la capa de dominio,
|
//! and its persistence contracts belong to the domain layer, following
|
||||||
//! siguiendo los principios de Clean/Hexagonal Architecture.
|
//! Clean/Hexagonal Architecture principles.
|
||||||
//!
|
//!
|
||||||
//! Las implementaciones concretas (filesystem, PostgreSQL, S3, etc.) viven en
|
//! Concrete implementations (filesystem, PostgreSQL, S3, etc.) live in
|
||||||
//! la capa de infraestructura.
|
//! the infrastructure layer.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
@@ -20,31 +20,31 @@ use crate::domain::services::path_service::StoragePath;
|
|||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
// FileReadRepository — operaciones de lectura/consulta
|
// FileReadRepository — read/query operations
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Puerto del dominio para **lectura** de archivos.
|
/// Domain port for file **reading**.
|
||||||
///
|
///
|
||||||
/// Encapsula toda operación que consulta estado sin modificarlo:
|
/// Encapsulates every operation that queries state without modifying it:
|
||||||
/// obtener, listar, contenido, stream, mmap, rango, resolución de rutas.
|
/// get, list, content, stream, mmap, range, path resolution.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FileReadRepository: Send + Sync + 'static {
|
pub trait FileReadRepository: Send + Sync + 'static {
|
||||||
/// Obtiene un archivo por su ID.
|
/// Gets a file by its ID.
|
||||||
async fn get_file(&self, id: &str) -> Result<File, DomainError>;
|
async fn get_file(&self, id: &str) -> Result<File, DomainError>;
|
||||||
|
|
||||||
/// Lista archivos en una carpeta.
|
/// Lists files in a folder.
|
||||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>;
|
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>;
|
||||||
|
|
||||||
/// Obtiene contenido completo como bytes (solo archivos pequeños/medianos).
|
/// Gets full content as bytes (only for small/medium files).
|
||||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
||||||
|
|
||||||
/// Obtiene contenido como stream (ideal para archivos grandes).
|
/// Gets content as a stream (ideal for large files).
|
||||||
async fn get_file_stream(
|
async fn get_file_stream(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||||
|
|
||||||
/// Stream de un rango de bytes (HTTP Range Requests, video seek).
|
/// Stream of a byte range (HTTP Range Requests, video seek).
|
||||||
async fn get_file_range_stream(
|
async fn get_file_range_stream(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
@@ -52,27 +52,27 @@ pub trait FileReadRepository: Send + Sync + 'static {
|
|||||||
end: Option<u64>,
|
end: Option<u64>,
|
||||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||||
|
|
||||||
/// Memory-map de archivo para acceso zero-copy (10–100 MB).
|
/// Memory-mapped file for zero-copy access (10–100 MB).
|
||||||
async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError>;
|
async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError>;
|
||||||
|
|
||||||
/// Obtiene la ruta de almacenamiento lógica de un archivo.
|
/// Gets the logical storage path of a file.
|
||||||
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||||
|
|
||||||
/// Obtiene el ID de la carpeta padre a partir de una ruta (WebDAV).
|
/// Gets the parent folder ID from a path (WebDAV).
|
||||||
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError>;
|
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
// FileWriteRepository — operaciones de escritura/mutación
|
// FileWriteRepository — write/mutation operations
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Puerto del dominio para **escritura** de archivos.
|
/// Domain port for file **writing**.
|
||||||
///
|
///
|
||||||
/// Cubre: upload (buffered + streaming), move, delete, update,
|
/// Covers: upload (buffered + streaming), move, delete, update,
|
||||||
/// y el registro diferido para write-behind cache.
|
/// and deferred registration for write-behind cache.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FileWriteRepository: Send + Sync + 'static {
|
pub trait FileWriteRepository: Send + Sync + 'static {
|
||||||
/// Guarda un nuevo archivo desde bytes.
|
/// Saves a new file from bytes.
|
||||||
async fn save_file(
|
async fn save_file(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -81,7 +81,7 @@ pub trait FileWriteRepository: Send + Sync + 'static {
|
|||||||
content: Vec<u8>,
|
content: Vec<u8>,
|
||||||
) -> Result<File, DomainError>;
|
) -> Result<File, DomainError>;
|
||||||
|
|
||||||
/// Upload en streaming — escribe chunks a disco sin acumular en RAM.
|
/// Streaming upload — writes chunks to disk without accumulating in RAM.
|
||||||
async fn save_file_from_stream(
|
async fn save_file_from_stream(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -90,30 +90,30 @@ pub trait FileWriteRepository: Send + Sync + 'static {
|
|||||||
stream: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
stream: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
||||||
) -> Result<File, DomainError>;
|
) -> Result<File, DomainError>;
|
||||||
|
|
||||||
/// Mueve un archivo a otra carpeta.
|
/// Moves a file to another folder.
|
||||||
async fn move_file(
|
async fn move_file(
|
||||||
&self,
|
&self,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
target_folder_id: Option<String>,
|
target_folder_id: Option<String>,
|
||||||
) -> Result<File, DomainError>;
|
) -> Result<File, DomainError>;
|
||||||
|
|
||||||
/// Renombra un archivo (same folder, different name).
|
/// Renames a file (same folder, different name).
|
||||||
async fn rename_file(
|
async fn rename_file(
|
||||||
&self,
|
&self,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
new_name: &str,
|
new_name: &str,
|
||||||
) -> Result<File, DomainError>;
|
) -> Result<File, DomainError>;
|
||||||
|
|
||||||
/// Elimina un archivo.
|
/// Deletes a file.
|
||||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Actualiza el contenido de un archivo existente.
|
/// Updates the content of an existing file.
|
||||||
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError>;
|
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Registra metadatos de archivo SIN escribir contenido a disco (write-behind).
|
/// Registers file metadata WITHOUT writing content to disk (write-behind).
|
||||||
///
|
///
|
||||||
/// Devuelve `(File, PathBuf)` donde `PathBuf` es la ruta destino para la
|
/// Returns `(File, PathBuf)` where `PathBuf` is the destination path for
|
||||||
/// escritura diferida que realizará el `WriteBehindCache`.
|
/// the deferred write that the `WriteBehindCache` will perform.
|
||||||
async fn register_file_deferred(
|
async fn register_file_deferred(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -124,27 +124,27 @@ pub trait FileWriteRepository: Send + Sync + 'static {
|
|||||||
|
|
||||||
// ── Trash operations ──
|
// ── Trash operations ──
|
||||||
|
|
||||||
/// Mueve un archivo a la papelera
|
/// Moves a file to the trash
|
||||||
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
|
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Restaura un archivo desde la papelera a su ubicación original
|
/// Restores a file from the trash to its original location
|
||||||
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>;
|
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Elimina un archivo permanentemente (usado por la papelera)
|
/// Permanently deletes a file (used by the trash)
|
||||||
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
|
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
// FileRepository — supertrait unificado
|
// FileRepository — unified supertrait
|
||||||
// ─────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Puerto unificado para persistencia de archivos.
|
/// Unified port for file persistence.
|
||||||
///
|
///
|
||||||
/// Es un supertrait de `FileReadRepository + FileWriteRepository`.
|
/// It is a supertrait of `FileReadRepository + FileWriteRepository`.
|
||||||
/// Cualquier tipo que implemente ambos ports obtiene `FileRepository`
|
/// Any type that implements both ports gets `FileRepository`
|
||||||
/// automáticamente vía blanket impl.
|
/// automatically via blanket impl.
|
||||||
pub trait FileRepository: FileReadRepository + FileWriteRepository {}
|
pub trait FileRepository: FileReadRepository + FileWriteRepository {}
|
||||||
|
|
||||||
/// Blanket implementation: cualquier tipo que implemente ambos ports
|
/// Blanket implementation: any type that implements both ports
|
||||||
/// es automáticamente un FileRepository.
|
/// is automatically a FileRepository.
|
||||||
impl<T: FileReadRepository + FileWriteRepository> FileRepository for T {}
|
impl<T: FileReadRepository + FileWriteRepository> FileRepository for T {}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
//! Puerto de persistencia del dominio para la entidad Folder.
|
//! Domain persistence port for the Folder entity.
|
||||||
//!
|
//!
|
||||||
//! Define el contrato que cualquier implementación de almacenamiento de carpetas
|
//! Defines the contract that any folder storage implementation
|
||||||
//! debe cumplir. Este trait vive en el dominio porque Folder es una entidad core
|
//! must fulfill. This trait lives in the domain because Folder is a core entity
|
||||||
//! del sistema y sus contratos de persistencia pertenecen a la capa de dominio,
|
//! of the system and its persistence contracts belong to the domain layer,
|
||||||
//! siguiendo los principios de Clean/Hexagonal Architecture.
|
//! following the principles of Clean/Hexagonal Architecture.
|
||||||
//!
|
//!
|
||||||
//! Las implementaciones concretas (filesystem, PostgreSQL, S3, etc.) viven en
|
//! Concrete implementations (filesystem, PostgreSQL, S3, etc.) live in
|
||||||
//! la capa de infraestructura.
|
//! the infrastructure layer.
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
@@ -14,25 +14,25 @@ use crate::domain::entities::folder::Folder;
|
|||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::StoragePath;
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
|
|
||||||
/// Puerto del dominio para persistencia de carpetas.
|
/// Domain port for folder persistence.
|
||||||
///
|
///
|
||||||
/// Define las operaciones CRUD y de gestión necesarias para
|
/// Defines the CRUD and management operations required for
|
||||||
/// la entidad Folder en el sistema de almacenamiento.
|
/// the Folder entity in the storage system.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FolderRepository: Send + Sync + 'static {
|
pub trait FolderRepository: Send + Sync + 'static {
|
||||||
/// Crea una nueva carpeta
|
/// Creates a new folder
|
||||||
async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError>;
|
async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError>;
|
||||||
|
|
||||||
/// Obtiene una carpeta por su ID
|
/// Gets a folder by its ID
|
||||||
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>;
|
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>;
|
||||||
|
|
||||||
/// Obtiene una carpeta por su ruta de almacenamiento
|
/// Gets a folder by its storage path
|
||||||
async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError>;
|
async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError>;
|
||||||
|
|
||||||
/// Lista carpetas dentro de una carpeta padre
|
/// Lists folders within a parent folder
|
||||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError>;
|
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError>;
|
||||||
|
|
||||||
/// Lista carpetas con paginación
|
/// Lists folders with pagination
|
||||||
async fn list_folders_paginated(
|
async fn list_folders_paginated(
|
||||||
&self,
|
&self,
|
||||||
parent_id: Option<&str>,
|
parent_id: Option<&str>,
|
||||||
@@ -41,29 +41,29 @@ pub trait FolderRepository: Send + Sync + 'static {
|
|||||||
include_total: bool
|
include_total: bool
|
||||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
|
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
|
||||||
|
|
||||||
/// Renombra una carpeta
|
/// Renames a folder
|
||||||
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
|
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
|
||||||
|
|
||||||
/// Mueve una carpeta a otro padre
|
/// Moves a folder to another parent
|
||||||
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> Result<Folder, DomainError>;
|
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> Result<Folder, DomainError>;
|
||||||
|
|
||||||
/// Elimina una carpeta
|
/// Deletes a folder
|
||||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
|
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Verifica si existe una carpeta en la ruta dada
|
/// Checks if a folder exists at the given path
|
||||||
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||||
|
|
||||||
/// Obtiene la ruta de una carpeta
|
/// Gets the path of a folder
|
||||||
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||||
|
|
||||||
// ── Trash operations ──
|
// ── Trash operations ──
|
||||||
|
|
||||||
/// Mueve una carpeta a la papelera
|
/// Moves a folder to the trash
|
||||||
async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError>;
|
async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Restaura una carpeta desde la papelera a su ubicación original
|
/// Restores a folder from the trash to its original location
|
||||||
async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> Result<(), DomainError>;
|
async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Elimina una carpeta permanentemente (usado por la papelera)
|
/// Permanently deletes a folder (used by the trash)
|
||||||
async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>;
|
async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ use crate::common::errors::DomainError;
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SessionRepositoryError {
|
pub enum SessionRepositoryError {
|
||||||
#[error("Sesión no encontrada: {0}")]
|
#[error("Session not found: {0}")]
|
||||||
NotFound(String),
|
NotFound(String),
|
||||||
|
|
||||||
#[error("Error de base de datos: {0}")]
|
#[error("Database error: {0}")]
|
||||||
DatabaseError(String),
|
DatabaseError(String),
|
||||||
|
|
||||||
#[error("Error de tiempo de espera: {0}")]
|
#[error("Timeout error: {0}")]
|
||||||
Timeout(String),
|
Timeout(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SessionRepositoryResult<T> = Result<T, SessionRepositoryError>;
|
pub type SessionRepositoryResult<T> = Result<T, SessionRepositoryError>;
|
||||||
|
|
||||||
// Conversión de SessionRepositoryError a DomainError
|
// Conversion from SessionRepositoryError to DomainError
|
||||||
impl From<SessionRepositoryError> for DomainError {
|
impl From<SessionRepositoryError> for DomainError {
|
||||||
fn from(err: SessionRepositoryError) -> Self {
|
fn from(err: SessionRepositoryError) -> Self {
|
||||||
match err {
|
match err {
|
||||||
@@ -35,24 +35,24 @@ impl From<SessionRepositoryError> for DomainError {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait SessionRepository: Send + Sync + 'static {
|
pub trait SessionRepository: Send + Sync + 'static {
|
||||||
/// Crea una nueva sesión
|
/// Creates a new session
|
||||||
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>;
|
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>;
|
||||||
|
|
||||||
/// Obtiene una sesión por ID
|
/// Gets a session by ID
|
||||||
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session>;
|
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session>;
|
||||||
|
|
||||||
/// Obtiene una sesión por token de actualización
|
/// Gets a session by refresh token
|
||||||
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult<Session>;
|
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult<Session>;
|
||||||
|
|
||||||
/// Obtiene todas las sesiones de un usuario
|
/// Gets all sessions for a user
|
||||||
async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>>;
|
async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>>;
|
||||||
|
|
||||||
/// Revoca una sesión específica
|
/// Revokes a specific session
|
||||||
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>;
|
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>;
|
||||||
|
|
||||||
/// Revoca todas las sesiones de un usuario
|
/// Revokes all sessions for a user
|
||||||
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64>;
|
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64>;
|
||||||
|
|
||||||
/// Elimina sesiones expiradas
|
/// Deletes expired sessions
|
||||||
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>;
|
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>;
|
||||||
}
|
}
|
||||||
@@ -4,28 +4,28 @@ use crate::common::errors::DomainError;
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum UserRepositoryError {
|
pub enum UserRepositoryError {
|
||||||
#[error("Usuario no encontrado: {0}")]
|
#[error("User not found: {0}")]
|
||||||
NotFound(String),
|
NotFound(String),
|
||||||
|
|
||||||
#[error("Usuario ya existe: {0}")]
|
#[error("User already exists: {0}")]
|
||||||
AlreadyExists(String),
|
AlreadyExists(String),
|
||||||
|
|
||||||
#[error("Error de base de datos: {0}")]
|
#[error("Database error: {0}")]
|
||||||
DatabaseError(String),
|
DatabaseError(String),
|
||||||
|
|
||||||
#[error("Error de validación: {0}")]
|
#[error("Validation error: {0}")]
|
||||||
ValidationError(String),
|
ValidationError(String),
|
||||||
|
|
||||||
#[error("Error de tiempo de espera: {0}")]
|
#[error("Timeout error: {0}")]
|
||||||
Timeout(String),
|
Timeout(String),
|
||||||
|
|
||||||
#[error("Operación no permitida: {0}")]
|
#[error("Operation not allowed: {0}")]
|
||||||
OperationNotAllowed(String),
|
OperationNotAllowed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>;
|
pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>;
|
||||||
|
|
||||||
// Conversión de UserRepositoryError a DomainError
|
// Conversion from UserRepositoryError to DomainError
|
||||||
impl From<UserRepositoryError> for DomainError {
|
impl From<UserRepositoryError> for DomainError {
|
||||||
fn from(err: UserRepositoryError) -> Self {
|
fn from(err: UserRepositoryError) -> Self {
|
||||||
match err {
|
match err {
|
||||||
@@ -53,59 +53,59 @@ impl From<UserRepositoryError> for DomainError {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait UserRepository: Send + Sync + 'static {
|
pub trait UserRepository: Send + Sync + 'static {
|
||||||
/// Crea un nuevo usuario
|
/// Creates a new user
|
||||||
async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
|
async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
|
||||||
|
|
||||||
/// Obtiene un usuario por ID
|
/// Gets a user by ID
|
||||||
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User>;
|
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User>;
|
||||||
|
|
||||||
/// Obtiene un usuario por nombre de usuario
|
/// Gets a user by username
|
||||||
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>;
|
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>;
|
||||||
|
|
||||||
/// Obtiene un usuario por correo electrónico
|
/// Gets a user by email
|
||||||
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User>;
|
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User>;
|
||||||
|
|
||||||
/// Actualiza un usuario existente
|
/// Updates an existing user
|
||||||
async fn update_user(&self, user: User) -> UserRepositoryResult<User>;
|
async fn update_user(&self, user: User) -> UserRepositoryResult<User>;
|
||||||
|
|
||||||
/// Actualiza solo el uso de almacenamiento de un usuario
|
/// Updates only a user's storage usage
|
||||||
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()>;
|
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()>;
|
||||||
|
|
||||||
/// Actualiza la fecha de último inicio de sesión
|
/// Updates the last login date
|
||||||
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()>;
|
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()>;
|
||||||
|
|
||||||
/// Lista usuarios con paginación
|
/// Lists users with pagination
|
||||||
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>>;
|
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>>;
|
||||||
|
|
||||||
/// Activa o desactiva un usuario
|
/// Activates or deactivates a user
|
||||||
async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()>;
|
async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()>;
|
||||||
|
|
||||||
/// Cambia la contraseña de un usuario
|
/// Changes a user's password
|
||||||
async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()>;
|
async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()>;
|
||||||
|
|
||||||
/// Cambia el rol de un usuario
|
/// Changes a user's role
|
||||||
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()>;
|
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()>;
|
||||||
|
|
||||||
/// Lista usuarios por rol (admin o user)
|
/// Lists users by role (admin or user)
|
||||||
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
|
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
|
||||||
|
|
||||||
/// Elimina un usuario
|
/// Deletes a user
|
||||||
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>;
|
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>;
|
||||||
|
|
||||||
/// Finds a user by OIDC provider + subject pair
|
/// Finds a user by OIDC provider + subject pair
|
||||||
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> UserRepositoryResult<User>;
|
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> UserRepositoryResult<User>;
|
||||||
|
|
||||||
/// Actualiza la cuota de almacenamiento de un usuario
|
/// Updates a user's storage quota
|
||||||
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()>;
|
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()>;
|
||||||
|
|
||||||
/// Cuenta el número total de usuarios
|
/// Counts the total number of users
|
||||||
async fn count_users(&self) -> UserRepositoryResult<i64>;
|
async fn count_users(&self) -> UserRepositoryResult<i64>;
|
||||||
|
|
||||||
/// Obtiene estadísticas de almacenamiento agregadas
|
/// Gets aggregated storage statistics
|
||||||
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats>;
|
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Estadísticas de almacenamiento agregadas
|
/// Aggregated storage statistics
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct StorageStats {
|
pub struct StorageStats {
|
||||||
pub total_users: i64,
|
pub total_users: i64,
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
//! StoragePath - Value Object del dominio para representar rutas de almacenamiento
|
//! StoragePath - Domain Value Object for representing storage paths
|
||||||
//!
|
//!
|
||||||
//! Este módulo contiene solo el Value Object StoragePath que es parte del dominio puro.
|
//! This module contains only the StoragePath Value Object which is part of the pure domain.
|
||||||
//! PathService (que implementa StoragePort y StorageMediator) fue movido a
|
//! PathService (which implements StoragePort and StorageMediator) was moved to
|
||||||
//! infrastructure/services/path_service.rs porque tiene dependencias de sistema de archivos.
|
//! infrastructure/services/path_service.rs because it has file system dependencies.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Representa una ruta de almacenamiento en el dominio (Value Object)
|
/// Represents a storage path in the domain (Value Object)
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub struct StoragePath {
|
pub struct StoragePath {
|
||||||
segments: Vec<String>,
|
segments: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StoragePath {
|
impl StoragePath {
|
||||||
/// Crea una nueva ruta de almacenamiento
|
/// Creates a new storage path
|
||||||
pub fn new(segments: Vec<String>) -> Self {
|
pub fn new(segments: Vec<String>) -> Self {
|
||||||
Self { segments }
|
Self { segments }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una ruta vacía (raíz)
|
/// Creates an empty path (root)
|
||||||
pub fn root() -> Self {
|
pub fn root() -> Self {
|
||||||
Self { segments: Vec::new() }
|
Self { segments: Vec::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una ruta a partir de una cadena con segmentos separados por /
|
/// Creates a path from a string with segments separated by /
|
||||||
pub fn from_string(path: &str) -> Self {
|
pub fn from_string(path: &str) -> Self {
|
||||||
let segments = path
|
let segments = path
|
||||||
.split('/')
|
.split('/')
|
||||||
@@ -33,7 +33,7 @@ impl StoragePath {
|
|||||||
Self { segments }
|
Self { segments }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una ruta a partir de un PathBuf
|
/// Creates a path from a PathBuf
|
||||||
pub fn from(path_buf: PathBuf) -> Self {
|
pub fn from(path_buf: PathBuf) -> Self {
|
||||||
let segments = path_buf
|
let segments = path_buf
|
||||||
.components()
|
.components()
|
||||||
@@ -45,19 +45,19 @@ impl StoragePath {
|
|||||||
Self { segments }
|
Self { segments }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un segmento a la ruta
|
/// Appends a segment to the path
|
||||||
pub fn join(&self, segment: &str) -> Self {
|
pub fn join(&self, segment: &str) -> Self {
|
||||||
let mut new_segments = self.segments.clone();
|
let mut new_segments = self.segments.clone();
|
||||||
new_segments.push(segment.to_string());
|
new_segments.push(segment.to_string());
|
||||||
Self { segments: new_segments }
|
Self { segments: new_segments }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene el nombre del archivo (último segmento)
|
/// Gets the file name (last segment)
|
||||||
pub fn file_name(&self) -> Option<String> {
|
pub fn file_name(&self) -> Option<String> {
|
||||||
self.segments.last().cloned()
|
self.segments.last().cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene la ruta del directorio padre
|
/// Gets the parent directory path
|
||||||
pub fn parent(&self) -> Option<Self> {
|
pub fn parent(&self) -> Option<Self> {
|
||||||
if self.segments.is_empty() {
|
if self.segments.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -67,12 +67,12 @@ impl StoragePath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifica si la ruta está vacía (es la raíz)
|
/// Checks if the path is empty (is the root)
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.segments.is_empty()
|
self.segments.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convierte la ruta a una cadena con formato "/segment1/segment2/..."
|
/// Converts the path to a string with format "/segment1/segment2/..."
|
||||||
pub fn to_string(&self) -> String {
|
pub fn to_string(&self) -> String {
|
||||||
if self.segments.is_empty() {
|
if self.segments.is_empty() {
|
||||||
"/".to_string()
|
"/".to_string()
|
||||||
@@ -81,15 +81,15 @@ impl StoragePath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve la representación de la ruta como cadena
|
/// Returns the path representation as a string
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
// Nota: La implementación realmente debería almacenar la cadena,
|
// Note: The implementation should really store the string,
|
||||||
// pero aquí hacemos una implementación temporal que siempre devuelve "/"
|
// but here we do a temporary implementation that always returns "/"
|
||||||
// Esto se usa solo para la implementación de get_folder_path_str
|
// This is only used for the get_folder_path_str implementation
|
||||||
"/"
|
"/"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene los segmentos de la ruta
|
/// Gets the path segments
|
||||||
pub fn segments(&self) -> &[String] {
|
pub fn segments(&self) -> &[String] {
|
||||||
&self.segments
|
&self.segments
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ pub async fn create_auth_services(
|
|||||||
pool: Arc<PgPool>,
|
pool: Arc<PgPool>,
|
||||||
folder_service: Option<Arc<FolderService>>
|
folder_service: Option<Arc<FolderService>>
|
||||||
) -> Result<AuthServices> {
|
) -> Result<AuthServices> {
|
||||||
// Crear servicio de tokens JWT (implementación de TokenServicePort)
|
// Create JWT token service (TokenServicePort implementation)
|
||||||
let token_service: Arc<dyn TokenServicePort> = Arc::new(JwtTokenService::new(
|
let token_service: Arc<dyn TokenServicePort> = Arc::new(JwtTokenService::new(
|
||||||
config.auth.jwt_secret.clone(),
|
config.auth.jwt_secret.clone(),
|
||||||
config.auth.access_token_expiry_secs,
|
config.auth.access_token_expiry_secs,
|
||||||
config.auth.refresh_token_expiry_secs,
|
config.auth.refresh_token_expiry_secs,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Crear servicio de hashing de contraseñas
|
// Create password hashing service
|
||||||
let password_hasher = Arc::new(Argon2PasswordHasher::new());
|
let password_hasher = Arc::new(Argon2PasswordHasher::new());
|
||||||
|
|
||||||
// Crear repositorios PostgreSQL
|
// Create PostgreSQL repositories
|
||||||
let user_repository = Arc::new(UserPgRepository::new(pool.clone()));
|
let user_repository = Arc::new(UserPgRepository::new(pool.clone()));
|
||||||
let session_repository = Arc::new(SessionPgRepository::new(pool.clone()));
|
let session_repository = Arc::new(SessionPgRepository::new(pool.clone()));
|
||||||
|
|
||||||
// Crear servicio de aplicación de autenticación
|
// Create authentication application service
|
||||||
let mut auth_app_service = AuthApplicationService::new(
|
let mut auth_app_service = AuthApplicationService::new(
|
||||||
user_repository,
|
user_repository,
|
||||||
session_repository,
|
session_repository,
|
||||||
@@ -39,7 +39,7 @@ pub async fn create_auth_services(
|
|||||||
token_service.clone(),
|
token_service.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Configurar servicio de carpetas si está disponible
|
// Configure folder service if available
|
||||||
if let Some(folder_svc) = folder_service {
|
if let Some(folder_svc) = folder_service {
|
||||||
auth_app_service = auth_app_service.with_folder_service(folder_svc);
|
auth_app_service = auth_app_service.with_folder_service(folder_svc);
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ pub async fn create_auth_services(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empaquetar servicio en Arc
|
// Package service in Arc
|
||||||
let auth_application_service = Arc::new(auth_app_service);
|
let auth_application_service = Arc::new(auth_app_service);
|
||||||
|
|
||||||
Ok(AuthServices {
|
Ok(AuthServices {
|
||||||
|
|||||||
+13
-13
@@ -4,7 +4,7 @@ use std::time::Duration;
|
|||||||
use crate::common::config::AppConfig;
|
use crate::common::config::AppConfig;
|
||||||
|
|
||||||
pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
||||||
tracing::info!("Inicializando conexión a PostgreSQL con URL: {}",
|
tracing::info!("Initializing PostgreSQL connection with URL: {}",
|
||||||
config.database.connection_string.replace("postgres://", "postgres://[user]:[pass]@"));
|
config.database.connection_string.replace("postgres://", "postgres://[user]:[pass]@"));
|
||||||
|
|
||||||
// Add a more robust connection attempt with retries
|
// Add a more robust connection attempt with retries
|
||||||
@@ -13,9 +13,9 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
|||||||
|
|
||||||
while attempt < MAX_ATTEMPTS {
|
while attempt < MAX_ATTEMPTS {
|
||||||
attempt += 1;
|
attempt += 1;
|
||||||
tracing::info!("Intento de conexión a PostgreSQL #{}", attempt);
|
tracing::info!("PostgreSQL connection attempt #{}", attempt);
|
||||||
|
|
||||||
// Crear el pool de conexiones con las opciones de configuración
|
// Create the connection pool with configuration options
|
||||||
match PgPoolOptions::new()
|
match PgPoolOptions::new()
|
||||||
.max_connections(config.database.max_connections)
|
.max_connections(config.database.max_connections)
|
||||||
.min_connections(config.database.min_connections)
|
.min_connections(config.database.min_connections)
|
||||||
@@ -25,10 +25,10 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
|||||||
.connect(&config.database.connection_string)
|
.connect(&config.database.connection_string)
|
||||||
.await {
|
.await {
|
||||||
Ok(pool) => {
|
Ok(pool) => {
|
||||||
// Verificar la conexión
|
// Verify the connection
|
||||||
match sqlx::query("SELECT 1").execute(&pool).await {
|
match sqlx::query("SELECT 1").execute(&pool).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!("Conexión a PostgreSQL establecida correctamente");
|
tracing::info!("PostgreSQL connection established successfully");
|
||||||
|
|
||||||
// Verify if migrations have been applied
|
// Verify if migrations have been applied
|
||||||
let migration_check = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')")
|
let migration_check = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')")
|
||||||
@@ -39,34 +39,34 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
|||||||
Ok(row) => {
|
Ok(row) => {
|
||||||
let tables_exist: bool = row.get(0);
|
let tables_exist: bool = row.get(0);
|
||||||
if !tables_exist {
|
if !tables_exist {
|
||||||
tracing::warn!("Las tablas de la base de datos no existen. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
|
tracing::warn!("Database tables do not exist. Please run migrations with: cargo run --bin migrate --features migrations");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
tracing::warn!("No se pudo verificar el estado de las migraciones. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
|
tracing::warn!("Could not verify migration status. Please run migrations with: cargo run --bin migrate --features migrations");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(pool);
|
return Ok(pool);
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Error al verificar conexión: {}", e);
|
tracing::error!("Error verifying connection: {}", e);
|
||||||
tracing::warn!("La base de datos parece no estar configurada. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
|
tracing::warn!("The database appears to not be configured. Please run migrations with: cargo run --bin migrate --features migrations");
|
||||||
if attempt >= MAX_ATTEMPTS {
|
if attempt >= MAX_ATTEMPTS {
|
||||||
return Err(anyhow::anyhow!("Error al verificar la conexión a PostgreSQL: {}", e));
|
return Err(anyhow::anyhow!("Error verifying PostgreSQL connection: {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Error al conectar a PostgreSQL: {}", e);
|
tracing::error!("Error connecting to PostgreSQL: {}", e);
|
||||||
if attempt >= MAX_ATTEMPTS {
|
if attempt >= MAX_ATTEMPTS {
|
||||||
return Err(anyhow::anyhow!("Error en la conexión a PostgreSQL: {}", e));
|
return Err(anyhow::anyhow!("Error in PostgreSQL connection: {}", e));
|
||||||
}
|
}
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(anyhow::anyhow!("No se pudo establecer la conexión a PostgreSQL después de {} intentos", MAX_ATTEMPTS))
|
Err(anyhow::anyhow!("Could not establish PostgreSQL connection after {} attempts", MAX_ATTEMPTS))
|
||||||
}
|
}
|
||||||
@@ -10,11 +10,11 @@ use crate::common::errors::DomainError;
|
|||||||
use crate::domain::entities::file::File;
|
use crate::domain::entities::file::File;
|
||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::StoragePath;
|
||||||
|
|
||||||
/// Composite que envuelve `Arc<dyn FileReadPort>` + `Arc<dyn FileWritePort>`
|
/// Composite that wraps `Arc<dyn FileReadPort>` + `Arc<dyn FileWritePort>`
|
||||||
/// y delega cada método al port correspondiente.
|
/// and delegates each method to the corresponding port.
|
||||||
///
|
///
|
||||||
/// Gracias al blanket impl `impl<T: FileReadPort + FileWritePort> FileStoragePort for T {}`
|
/// Thanks to the blanket impl `impl<T: FileReadPort + FileWritePort> FileStoragePort for T {}`
|
||||||
/// este tipo obtiene `FileStoragePort` automáticamente.
|
/// this type automatically gets `FileStoragePort`.
|
||||||
pub struct CompositeFileRepository {
|
pub struct CompositeFileRepository {
|
||||||
read: Arc<dyn FileReadPort>,
|
read: Arc<dyn FileReadPort>,
|
||||||
write: Arc<dyn FileWritePort>,
|
write: Arc<dyn FileWritePort>,
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ use crate::infrastructure::services::path_service::PathService;
|
|||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::StoragePath;
|
||||||
use crate::common::config::AppConfig;
|
use crate::common::config::AppConfig;
|
||||||
|
|
||||||
/// Implementación de repositorio para operaciones de **lectura** de archivos.
|
/// Repository implementation for file **read** operations.
|
||||||
///
|
///
|
||||||
/// Implementa `FileReadPort`:
|
/// Implements `FileReadPort`:
|
||||||
/// get_file, list_files, get_file_content, get_file_stream,
|
/// get_file, list_files, get_file_content, get_file_stream,
|
||||||
/// get_file_range_stream, get_file_mmap, get_file_path, get_parent_folder_id.
|
/// get_file_range_stream, get_file_mmap, get_file_path, get_parent_folder_id.
|
||||||
pub struct FileFsReadRepository {
|
pub struct FileFsReadRepository {
|
||||||
@@ -37,7 +37,7 @@ pub struct FileFsReadRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileFsReadRepository {
|
impl FileFsReadRepository {
|
||||||
/// Constructor completo con todas las dependencias de infraestructura.
|
/// Full constructor with all infrastructure dependencies.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
root_path: PathBuf,
|
root_path: PathBuf,
|
||||||
storage_mediator: Arc<dyn StorageMediator>,
|
storage_mediator: Arc<dyn StorageMediator>,
|
||||||
@@ -58,7 +58,7 @@ impl FileFsReadRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stub para pruebas (no realiza I/O real).
|
/// Stub for testing (does not perform real I/O).
|
||||||
pub fn default_stub() -> Self {
|
pub fn default_stub() -> Self {
|
||||||
Self {
|
Self {
|
||||||
root_path: PathBuf::from("./storage"),
|
root_path: PathBuf::from("./storage"),
|
||||||
@@ -75,7 +75,7 @@ impl FileFsReadRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── helpers internos ────────────────────────────────────
|
// ─── internal helpers ─────────────────────────────────────
|
||||||
|
|
||||||
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
|
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||||
self.path_service.resolve_path(storage_path)
|
self.path_service.resolve_path(storage_path)
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ use crate::infrastructure::services::path_service::PathService;
|
|||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::StoragePath;
|
||||||
use crate::common::config::AppConfig;
|
use crate::common::config::AppConfig;
|
||||||
|
|
||||||
/// Implementación de repositorio para operaciones de **escritura** de archivos.
|
/// Repository implementation for file **write** operations.
|
||||||
///
|
///
|
||||||
/// Implementa `FileWritePort`:
|
/// Implements `FileWritePort`:
|
||||||
/// save_file, save_file_from_stream, move_file, delete_file,
|
/// save_file, save_file_from_stream, move_file, delete_file,
|
||||||
/// update_file_content, register_file_deferred.
|
/// update_file_content, register_file_deferred.
|
||||||
pub struct FileFsWriteRepository {
|
pub struct FileFsWriteRepository {
|
||||||
@@ -37,7 +37,7 @@ pub struct FileFsWriteRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileFsWriteRepository {
|
impl FileFsWriteRepository {
|
||||||
/// Constructor completo con todas las dependencias.
|
/// Full constructor with all dependencies.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
root_path: PathBuf,
|
root_path: PathBuf,
|
||||||
storage_mediator: Arc<dyn StorageMediator>,
|
storage_mediator: Arc<dyn StorageMediator>,
|
||||||
@@ -50,7 +50,7 @@ impl FileFsWriteRepository {
|
|||||||
Self { root_path, storage_mediator, id_mapping_service, path_service, metadata_cache, config, parallel_processor }
|
Self { root_path, storage_mediator, id_mapping_service, path_service, metadata_cache, config, parallel_processor }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stub para pruebas (no realiza I/O real).
|
/// Stub for testing (does not perform real I/O).
|
||||||
pub fn default_stub() -> Self {
|
pub fn default_stub() -> Self {
|
||||||
Self {
|
Self {
|
||||||
root_path: PathBuf::from("./storage"),
|
root_path: PathBuf::from("./storage"),
|
||||||
|
|||||||
@@ -5,147 +5,147 @@ use tracing::{debug, error};
|
|||||||
use crate::infrastructure::repositories::repository_errors::FolderRepositoryResult;
|
use crate::infrastructure::repositories::repository_errors::FolderRepositoryResult;
|
||||||
use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository;
|
use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository;
|
||||||
|
|
||||||
// Este archivo contiene la implementación de los métodos relacionados con la papelera
|
// This file contains the implementation of trash-related methods
|
||||||
// para el repositorio de carpetas FolderFsRepository
|
// for the FolderFsRepository folder repository
|
||||||
|
|
||||||
// Implementación de métodos de papelera para el repositorio de carpetas
|
// Implementation of trash methods for the folder repository
|
||||||
impl FolderFsRepository {
|
impl FolderFsRepository {
|
||||||
// Obtiene la ruta completa a la papelera
|
// Gets the full path to the trash directory
|
||||||
fn get_trash_dir(&self) -> PathBuf {
|
fn get_trash_dir(&self) -> PathBuf {
|
||||||
self.get_root_path().join(".trash").join("folders")
|
self.get_root_path().join(".trash").join("folders")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crea una ruta única en la papelera para la carpeta
|
// Creates a unique path in the trash for the folder
|
||||||
async fn create_trash_folder_path(&self, folder_id: &str) -> FolderRepositoryResult<PathBuf> {
|
async fn create_trash_folder_path(&self, folder_id: &str) -> FolderRepositoryResult<PathBuf> {
|
||||||
let trash_dir = self.get_trash_dir();
|
let trash_dir = self.get_trash_dir();
|
||||||
|
|
||||||
// Asegurarse que el directorio de la papelera existe
|
// Ensure the trash directory exists
|
||||||
if !trash_dir.exists() {
|
if !trash_dir.exists() {
|
||||||
fs::create_dir_all(&trash_dir).await
|
fs::create_dir_all(&trash_dir).await
|
||||||
.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?;
|
.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crear una ruta única para la carpeta en la papelera
|
// Create a unique path for the folder in the trash
|
||||||
Ok(trash_dir.join(folder_id))
|
Ok(trash_dir.join(folder_id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementación de los métodos públicos del trait FolderRepository relacionados con la papelera
|
// Implementation of public FolderRepository trait methods related to trash
|
||||||
// Implementation of internal methods for trash functionality
|
// Implementation of internal methods for trash functionality
|
||||||
// These will be enabled when the trash feature is re-enabled
|
// These will be enabled when the trash feature is re-enabled
|
||||||
impl FolderFsRepository {
|
impl FolderFsRepository {
|
||||||
/// Helper method that will be used for trash functionality
|
/// Helper method that will be used for trash functionality
|
||||||
pub(crate) async fn _trash_move_to_trash(&self, folder_id: &str) -> FolderRepositoryResult<()> {
|
pub(crate) async fn _trash_move_to_trash(&self, folder_id: &str) -> FolderRepositoryResult<()> {
|
||||||
debug!("Moviendo carpeta a la papelera: {}", folder_id);
|
debug!("Moving folder to trash: {}", folder_id);
|
||||||
|
|
||||||
// Obtener la ruta física de la carpeta
|
// Get the physical path of the folder
|
||||||
let folder_path = match self.get_mapped_folder_path(folder_id).await {
|
let folder_path = match self.get_mapped_folder_path(folder_id).await {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error obteniendo ruta de la carpeta {}: {:?}", folder_id, e);
|
error!("Error getting folder path {}: {:?}", folder_id, e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let folder_path_buf = PathBuf::from(folder_path.to_string());
|
let folder_path_buf = PathBuf::from(folder_path.to_string());
|
||||||
|
|
||||||
// Verificamos que la carpeta existe
|
// Verify the folder exists
|
||||||
if !folder_path_buf.exists() {
|
if !folder_path_buf.exists() {
|
||||||
return Err(FolderRepositoryError::NotFound(format!("Folder not found: {}", folder_id)));
|
return Err(FolderRepositoryError::NotFound(format!("Folder not found: {}", folder_id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crear directorio en la papelera
|
// Create directory in the trash
|
||||||
let trash_folder_path = self.create_trash_folder_path(folder_id).await?;
|
let trash_folder_path = self.create_trash_folder_path(folder_id).await?;
|
||||||
|
|
||||||
// Mover la carpeta físicamente a la papelera
|
// Physically move the folder to the trash
|
||||||
match fs::rename(&folder_path_buf, &trash_folder_path).await {
|
match fs::rename(&folder_path_buf, &trash_folder_path).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Carpeta movida a papelera: {} -> {}", folder_path_buf.display(), trash_folder_path.display());
|
debug!("Folder moved to trash: {} -> {}", folder_path_buf.display(), trash_folder_path.display());
|
||||||
|
|
||||||
// Actualizar el mapeo al nuevo path en la papelera
|
// Update the mapping to the new path in the trash
|
||||||
if let Err(e) = self.update_mapped_folder_path(folder_id, &trash_folder_path).await {
|
if let Err(e) = self.update_mapped_folder_path(folder_id, &trash_folder_path).await {
|
||||||
error!("Error actualizando mapeo de carpeta en papelera: {}", e);
|
error!("Error updating folder mapping in trash: {}", e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error moviendo carpeta a papelera: {}", e);
|
error!("Error moving folder to trash: {}", e);
|
||||||
Err(FolderRepositoryError::StorageError(e.to_string()))
|
Err(FolderRepositoryError::StorageError(e.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restaura una carpeta desde la papelera a su ubicación original
|
/// Restores a folder from the trash to its original location
|
||||||
pub(crate) async fn _trash_restore_from_trash(&self, folder_id: &str, original_path: &str) -> FolderRepositoryResult<()> {
|
pub(crate) async fn _trash_restore_from_trash(&self, folder_id: &str, original_path: &str) -> FolderRepositoryResult<()> {
|
||||||
debug!("Restaurando carpeta {} a {}", folder_id, original_path);
|
debug!("Restoring folder {} to {}", folder_id, original_path);
|
||||||
|
|
||||||
// Obtener la ruta actual en la papelera
|
// Get the current path in the trash
|
||||||
let current_path = match self.get_mapped_folder_path(folder_id).await {
|
let current_path = match self.get_mapped_folder_path(folder_id).await {
|
||||||
Ok(path) => PathBuf::from(path),
|
Ok(path) => PathBuf::from(path),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error obteniendo ruta actual de la carpeta {}: {:?}", folder_id, e);
|
error!("Error getting current folder path {}: {:?}", folder_id, e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convertir la ruta original a PathBuf
|
// Convert the original path to PathBuf
|
||||||
let original_path_buf = PathBuf::from(original_path);
|
let original_path_buf = PathBuf::from(original_path);
|
||||||
|
|
||||||
// Asegurar que el directorio padre de destino existe
|
// Ensure the destination parent directory exists
|
||||||
if let Some(parent) = original_path_buf.parent() {
|
if let Some(parent) = original_path_buf.parent() {
|
||||||
if !parent.exists() {
|
if !parent.exists() {
|
||||||
fs::create_dir_all(parent).await
|
fs::create_dir_all(parent).await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("Error creando directorio padre para restauración: {}", e);
|
error!("Error creating parent directory for restoration: {}", e);
|
||||||
FolderRepositoryError::StorageError(e.to_string())
|
FolderRepositoryError::StorageError(e.to_string())
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mover la carpeta de la papelera a su ubicación original
|
// Move the folder from the trash to its original location
|
||||||
match fs::rename(¤t_path, &original_path_buf).await {
|
match fs::rename(¤t_path, &original_path_buf).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Carpeta restaurada: {} -> {}", current_path.display(), original_path_buf.display());
|
debug!("Folder restored: {} -> {}", current_path.display(), original_path_buf.display());
|
||||||
|
|
||||||
// Actualizar el mapeo a la ruta original
|
// Update the mapping to the original path
|
||||||
if let Err(e) = self.update_mapped_folder_path(folder_id, &original_path_buf).await {
|
if let Err(e) = self.update_mapped_folder_path(folder_id, &original_path_buf).await {
|
||||||
error!("Error actualizando mapeo de carpeta restaurada: {}", e);
|
error!("Error updating restored folder mapping: {}", e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error restaurando carpeta: {}", e);
|
error!("Error restoring folder: {}", e);
|
||||||
Err(FolderRepositoryError::StorageError(e.to_string()))
|
Err(FolderRepositoryError::StorageError(e.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina una carpeta permanentemente (usado por la papelera)
|
/// Permanently deletes a folder (used by the trash)
|
||||||
pub(crate) async fn _trash_delete_folder_permanently(&self, folder_id: &str) -> FolderRepositoryResult<()> {
|
pub(crate) async fn _trash_delete_folder_permanently(&self, folder_id: &str) -> FolderRepositoryResult<()> {
|
||||||
debug!("Eliminando carpeta permanentemente: {}", folder_id);
|
debug!("Permanently deleting folder: {}", folder_id);
|
||||||
|
|
||||||
// Similar a delete_folder pero sin validaciones adicionales
|
// Similar to delete_folder but without additional validations
|
||||||
let folder_path = match self.get_mapped_folder_path(folder_id).await {
|
let folder_path = match self.get_mapped_folder_path(folder_id).await {
|
||||||
Ok(path) => PathBuf::from(path),
|
Ok(path) => PathBuf::from(path),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error obteniendo ruta de la carpeta {}: {:?}", folder_id, e);
|
error!("Error getting folder path {}: {:?}", folder_id, e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Eliminar la carpeta recursivamente
|
// Delete the folder recursively
|
||||||
if folder_path.exists() {
|
if folder_path.exists() {
|
||||||
match fs::remove_dir_all(&folder_path).await {
|
match fs::remove_dir_all(&folder_path).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Carpeta eliminada permanentemente: {}", folder_path.display());
|
debug!("Folder permanently deleted: {}", folder_path.display());
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error eliminando carpeta permanentemente: {}", e);
|
error!("Error permanently deleting folder: {}", e);
|
||||||
// No reportar error si la carpeta ya no existe
|
// Don't report error if the folder no longer exists
|
||||||
if e.kind() != std::io::ErrorKind::NotFound {
|
if e.kind() != std::io::ErrorKind::NotFound {
|
||||||
return Err(FolderRepositoryError::StorageError(e.to_string()));
|
return Err(FolderRepositoryError::StorageError(e.to_string()));
|
||||||
}
|
}
|
||||||
@@ -153,16 +153,16 @@ impl FolderFsRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eliminar el mapeo
|
// Remove the mapping
|
||||||
if let Err(e) = self.remove_mapped_folder_id(folder_id).await {
|
if let Err(e) = self.remove_mapped_folder_id(folder_id).await {
|
||||||
error!("Error eliminando mapeo de la carpeta: {}", e);
|
error!("Error removing folder mapping: {}", e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Carpeta eliminada permanentemente con éxito: {}", folder_id);
|
debug!("Folder permanently deleted successfully: {}", folder_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-exportaciones necesarias para el compilador
|
// Re-exports needed by the compiler
|
||||||
use crate::infrastructure::repositories::repository_errors::FolderRepositoryError;
|
use crate::infrastructure::repositories::repository_errors::FolderRepositoryError;
|
||||||
@@ -20,9 +20,9 @@ impl CalendarEventPgRepository {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CalendarEventRepository for CalendarEventPgRepository {
|
impl CalendarEventRepository for CalendarEventPgRepository {
|
||||||
async fn create_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent> {
|
async fn create_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent> {
|
||||||
// Este método necesitaría una implementación completa que construya el CalendarEvent
|
// This method would need a full implementation that builds the CalendarEvent
|
||||||
// desde el resultado de la query, utilizando métodos del constructor
|
// from the query result, using constructor methods
|
||||||
// Para esta demostración, vamos a retornar el mismo evento
|
// For this demonstration, we return the same event
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -50,7 +50,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::database_error(format!("Failed to create calendar event: {}", e)))?;
|
.map_err(|e| DomainError::database_error(format!("Failed to create calendar event: {}", e)))?;
|
||||||
|
|
||||||
// Devolvemos el mismo evento en vez de un resultado
|
// We return the same event instead of a result
|
||||||
Ok(event)
|
Ok(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +86,8 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::database_error(format!("Failed to update calendar event: {}", e)))?;
|
.map_err(|e| DomainError::database_error(format!("Failed to update calendar event: {}", e)))?;
|
||||||
|
|
||||||
// En una implementación completa, recuperaríamos el evento actualizado
|
// In a full implementation, we would retrieve the updated event
|
||||||
// Por simplicidad, devolvemos el mismo evento que recibimos
|
// For simplicity, we return the same event we received
|
||||||
Ok(event)
|
Ok(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,9 +176,9 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
|||||||
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by id: {}", e)))?
|
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by id: {}", e)))?
|
||||||
.ok_or_else(|| DomainError::not_found("Calendar Event", id.to_string()))?;
|
.ok_or_else(|| DomainError::not_found("Calendar Event", id.to_string()))?;
|
||||||
|
|
||||||
// En una implementación real, construiríamos un objeto CalendarEvent completo
|
// In a real implementation, we would build a complete CalendarEvent object
|
||||||
// Por simplicidad, creamos un objeto con valores predeterminados para
|
// For simplicity, we create an object with default values to
|
||||||
// demostrar el enfoque sin macros
|
// demonstrate the approach without macros
|
||||||
|
|
||||||
let event = CalendarEvent::with_id(
|
let event = CalendarEvent::with_id(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
.bind(calendar.owner_id())
|
.bind(calendar.owner_id())
|
||||||
.bind(calendar.description())
|
.bind(calendar.description())
|
||||||
.bind(calendar.color())
|
.bind(calendar.color())
|
||||||
.bind(false) // is_public no existe como campo
|
.bind(false) // is_public doesn't exist as a field
|
||||||
.bind(calendar.created_at())
|
.bind(calendar.created_at())
|
||||||
.bind(calendar.updated_at())
|
.bind(calendar.updated_at())
|
||||||
.fetch_one(&*self.pool)
|
.fetch_one(&*self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::database_error(format!("Failed to create calendar: {}", e)))?;
|
.map_err(|e| DomainError::database_error(format!("Failed to create calendar: {}", e)))?;
|
||||||
|
|
||||||
// Construir el objeto Calendar utilizando su constructor with_id
|
// Build the Calendar object using its with_id constructor
|
||||||
let result = Calendar::with_id(
|
let result = Calendar::with_id(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
row.get("name"),
|
row.get("name"),
|
||||||
@@ -66,14 +66,14 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
.bind(calendar.name())
|
.bind(calendar.name())
|
||||||
.bind(calendar.description())
|
.bind(calendar.description())
|
||||||
.bind(calendar.color())
|
.bind(calendar.color())
|
||||||
.bind(false) // is_public no existe como campo
|
.bind(false) // is_public doesn't exist as a field
|
||||||
.bind(now)
|
.bind(now)
|
||||||
.bind(calendar.id())
|
.bind(calendar.id())
|
||||||
.fetch_one(&*self.pool)
|
.fetch_one(&*self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::database_error(format!("Failed to update calendar: {}", e)))?;
|
.map_err(|e| DomainError::database_error(format!("Failed to update calendar: {}", e)))?;
|
||||||
|
|
||||||
// Construir el objeto Calendar utilizando su constructor with_id
|
// Build the Calendar object using its with_id constructor
|
||||||
let result = Calendar::with_id(
|
let result = Calendar::with_id(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
row.get("name"),
|
row.get("name"),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::application::dtos::favorites_dto::FavoriteItemDto;
|
|||||||
use crate::application::ports::favorites_ports::FavoritesRepositoryPort;
|
use crate::application::ports::favorites_ports::FavoritesRepositoryPort;
|
||||||
use crate::common::errors::{Result, DomainError, ErrorKind};
|
use crate::common::errors::{Result, DomainError, ErrorKind};
|
||||||
|
|
||||||
/// Implementación PostgreSQL del puerto de persistencia de favoritos.
|
/// PostgreSQL implementation of the favorites persistence port.
|
||||||
pub struct FavoritesPgRepository {
|
pub struct FavoritesPgRepository {
|
||||||
db_pool: Arc<PgPool>,
|
db_pool: Arc<PgPool>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::application::dtos::recent_dto::RecentItemDto;
|
|||||||
use crate::application::ports::recent_ports::RecentItemsRepositoryPort;
|
use crate::application::ports::recent_ports::RecentItemsRepositoryPort;
|
||||||
use crate::common::errors::{Result, DomainError, ErrorKind};
|
use crate::common::errors::{Result, DomainError, ErrorKind};
|
||||||
|
|
||||||
/// Implementación PostgreSQL del puerto de persistencia de elementos recientes.
|
/// PostgreSQL implementation of the recent items persistence port.
|
||||||
pub struct RecentItemsPgRepository {
|
pub struct RecentItemsPgRepository {
|
||||||
db_pool: Arc<PgPool>,
|
db_pool: Arc<PgPool>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::application::ports::auth_ports::SessionStoragePort;
|
|||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
|
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
|
||||||
|
|
||||||
// Implementar From<sqlx::Error> para SessionRepositoryError para permitir conversiones automáticas
|
// Implement From<sqlx::Error> for SessionRepositoryError to allow automatic conversions
|
||||||
impl From<sqlx::Error> for SessionRepositoryError {
|
impl From<sqlx::Error> for SessionRepositoryError {
|
||||||
fn from(err: sqlx::Error) -> Self {
|
fn from(err: sqlx::Error) -> Self {
|
||||||
SessionPgRepository::map_sqlx_error(err)
|
SessionPgRepository::map_sqlx_error(err)
|
||||||
@@ -26,14 +26,14 @@ impl SessionPgRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Método auxiliar para mapear errores SQL a errores de dominio
|
// Helper method to map SQL errors to domain errors
|
||||||
pub fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
|
pub fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
|
||||||
match err {
|
match err {
|
||||||
sqlx::Error::RowNotFound => {
|
sqlx::Error::RowNotFound => {
|
||||||
SessionRepositoryError::NotFound("Sesión no encontrada".to_string())
|
SessionRepositoryError::NotFound("Session not found".to_string())
|
||||||
},
|
},
|
||||||
_ => SessionRepositoryError::DatabaseError(
|
_ => SessionRepositoryError::DatabaseError(
|
||||||
format!("Error de base de datos: {}", err)
|
format!("Database error: {}", err)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,9 +41,9 @@ impl SessionPgRepository {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl SessionRepository for SessionPgRepository {
|
impl SessionRepository for SessionPgRepository {
|
||||||
/// Crea una nueva sesión utilizando una transacción
|
/// Creates a new session using a transaction
|
||||||
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session> {
|
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session> {
|
||||||
// Crear una copia de la sesión para el closure
|
// Create a copy of the session for the closure
|
||||||
let session_clone = session.clone();
|
let session_clone = session.clone();
|
||||||
|
|
||||||
with_transaction(
|
with_transaction(
|
||||||
@@ -51,7 +51,7 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
"create_session",
|
"create_session",
|
||||||
|tx| {
|
|tx| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// Insertar la sesión
|
// Insert the session
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO auth.sessions (
|
INSERT INTO auth.sessions (
|
||||||
@@ -74,8 +74,8 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
.await
|
.await
|
||||||
.map_err(Self::map_sqlx_error)?;
|
.map_err(Self::map_sqlx_error)?;
|
||||||
|
|
||||||
// Opcionalmente, actualizar el último login del usuario
|
// Optionally, update the user's last login
|
||||||
// dentro de la misma transacción
|
// within the same transaction
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
UPDATE auth.users
|
UPDATE auth.users
|
||||||
@@ -87,12 +87,12 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
// Convertimos el error pero sin interrumpir la creación
|
// Convert the error but without interrupting session
|
||||||
// de la sesión si falla la actualización
|
// creation if the update fails
|
||||||
tracing::warn!("No se pudo actualizar last_login_at para usuario {}: {}",
|
tracing::warn!("Could not update last_login_at for user {}: {}",
|
||||||
session_clone.user_id(), e);
|
session_clone.user_id(), e);
|
||||||
SessionRepositoryError::DatabaseError(format!(
|
SessionRepositoryError::DatabaseError(format!(
|
||||||
"Sesión creada pero no se pudo actualizar last_login_at: {}", e
|
"Session created but could not update last_login_at: {}", e
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene una sesión por ID
|
/// Gets a session by ID
|
||||||
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session> {
|
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -132,7 +132,7 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene una sesión por token de actualización
|
/// Gets a session by refresh token
|
||||||
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult<Session> {
|
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult<Session> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -160,7 +160,7 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene todas las sesiones de un usuario
|
/// Gets all sessions for a user
|
||||||
async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>> {
|
async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -195,16 +195,16 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
Ok(sessions)
|
Ok(sessions)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Revoca una sesión específica utilizando una transacción
|
/// Revokes a specific session using a transaction
|
||||||
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> {
|
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> {
|
||||||
let id = session_id.to_string(); // Clone para uso en closure
|
let id = session_id.to_string(); // Clone for use in closure
|
||||||
|
|
||||||
with_transaction(
|
with_transaction(
|
||||||
&self.pool,
|
&self.pool,
|
||||||
"revoke_session",
|
"revoke_session",
|
||||||
|tx| {
|
|tx| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// Revocar la sesión
|
// Revoke the session
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
UPDATE auth.sessions
|
UPDATE auth.sessions
|
||||||
@@ -218,14 +218,14 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
.await
|
.await
|
||||||
.map_err(Self::map_sqlx_error)?;
|
.map_err(Self::map_sqlx_error)?;
|
||||||
|
|
||||||
// Si encontramos la sesión, podemos registrar un evento de seguridad
|
// If we found the session, we can log a security event
|
||||||
if let Some(row) = result {
|
if let Some(row) = result {
|
||||||
let user_id: String = row.try_get("user_id").unwrap_or_default();
|
let user_id: String = row.try_get("user_id").unwrap_or_default();
|
||||||
|
|
||||||
// Registrar evento de seguridad (en una tabla de seguridad)
|
// Log security event (in a security table)
|
||||||
// Esto es opcional pero muestra cómo se puede realizar operaciones
|
// This is optional but shows how additional operations
|
||||||
// adicionales en la misma transacción
|
// can be performed in the same transaction
|
||||||
tracing::info!("Sesión con ID {} del usuario {} revocada", id, user_id);
|
tracing::info!("Session with ID {} for user {} revoked", id, user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -234,16 +234,16 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
).await
|
).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Revoca todas las sesiones de un usuario utilizando una transacción
|
/// Revokes all sessions for a user using a transaction
|
||||||
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64> {
|
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64> {
|
||||||
let user_id_clone = user_id.to_string(); // Clone para uso en closure
|
let user_id_clone = user_id.to_string(); // Clone for use in closure
|
||||||
|
|
||||||
with_transaction(
|
with_transaction(
|
||||||
&self.pool,
|
&self.pool,
|
||||||
"revoke_all_user_sessions",
|
"revoke_all_user_sessions",
|
||||||
|tx| {
|
|tx| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// Revocar todas las sesiones del usuario
|
// Revoke all sessions for the user
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
UPDATE auth.sessions
|
UPDATE auth.sessions
|
||||||
@@ -258,9 +258,9 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
|
|
||||||
let affected = result.rows_affected();
|
let affected = result.rows_affected();
|
||||||
|
|
||||||
// Registrar evento de seguridad
|
// Log security event
|
||||||
if affected > 0 {
|
if affected > 0 {
|
||||||
tracing::info!("Revocadas {} sesiones del usuario {}", affected, user_id_clone);
|
tracing::info!("Revoked {} sessions for user {}", affected, user_id_clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(affected)
|
Ok(affected)
|
||||||
@@ -269,7 +269,7 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
).await
|
).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina sesiones expiradas
|
/// Deletes expired sessions
|
||||||
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64> {
|
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64> {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementación del puerto de almacenamiento para la capa de aplicación
|
// Implementation of the storage port for the application layer
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl SessionStoragePort for SessionPgRepository {
|
impl SessionStoragePort for SessionPgRepository {
|
||||||
async fn create_session(&self, session: Session) -> Result<Session, DomainError> {
|
async fn create_session(&self, session: Session) -> Result<Session, DomainError> {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::application::ports::auth_ports::UserStoragePort;
|
|||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
|
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
|
||||||
|
|
||||||
// Implementar From<sqlx::Error> para UserRepositoryError para permitir conversiones automáticas
|
// Implement From<sqlx::Error> for UserRepositoryError to allow automatic conversions
|
||||||
impl From<sqlx::Error> for UserRepositoryError {
|
impl From<sqlx::Error> for UserRepositoryError {
|
||||||
fn from(err: sqlx::Error) -> Self {
|
fn from(err: sqlx::Error) -> Self {
|
||||||
UserPgRepository::map_sqlx_error(err)
|
UserPgRepository::map_sqlx_error(err)
|
||||||
@@ -25,26 +25,26 @@ impl UserPgRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Método auxiliar para mapear errores SQL a errores de dominio
|
// Helper method to map SQL errors to domain errors
|
||||||
pub fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError {
|
pub fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError {
|
||||||
match err {
|
match err {
|
||||||
sqlx::Error::RowNotFound => {
|
sqlx::Error::RowNotFound => {
|
||||||
UserRepositoryError::NotFound("Usuario no encontrado".to_string())
|
UserRepositoryError::NotFound("User not found".to_string())
|
||||||
},
|
},
|
||||||
sqlx::Error::Database(db_err) => {
|
sqlx::Error::Database(db_err) => {
|
||||||
if db_err.code().map_or(false, |code| code == "23505") {
|
if db_err.code().map_or(false, |code| code == "23505") {
|
||||||
// Código para violación de unicidad en PostgreSQL
|
// PostgreSQL uniqueness violation code
|
||||||
UserRepositoryError::AlreadyExists(
|
UserRepositoryError::AlreadyExists(
|
||||||
"Usuario o email ya existe".to_string()
|
"User or email already exists".to_string()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
UserRepositoryError::DatabaseError(
|
UserRepositoryError::DatabaseError(
|
||||||
format!("Error de base de datos: {}", db_err)
|
format!("Database error: {}", db_err)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => UserRepositoryError::DatabaseError(
|
_ => UserRepositoryError::DatabaseError(
|
||||||
format!("Error de base de datos: {}", err)
|
format!("Database error: {}", err)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ impl UserPgRepository {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl UserRepository for UserPgRepository {
|
impl UserRepository for UserPgRepository {
|
||||||
/// Crea un nuevo usuario utilizando una transacción
|
/// Creates a new user using a transaction
|
||||||
async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
|
async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
|
||||||
// Creamos una copia del usuario para el closure
|
// Creamos una copia del usuario para el closure
|
||||||
let user_clone = user.clone();
|
let user_clone = user.clone();
|
||||||
@@ -61,14 +61,14 @@ impl UserRepository for UserPgRepository {
|
|||||||
&self.pool,
|
&self.pool,
|
||||||
"create_user",
|
"create_user",
|
||||||
|tx| {
|
|tx| {
|
||||||
// Necesitamos mover el closure a un BoxFuture para devolver dentro
|
// We need to move the closure into a BoxFuture to return inside
|
||||||
// de la llamada with_transaction
|
// the with_transaction call
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// Usamos los getters para extraer los valores
|
// Use getters to extract the values
|
||||||
// Convertimos user.role() a string para pasarlo como texto plano
|
// Convert user.role() to string to pass it as plain text
|
||||||
let role_str = user_clone.role().to_string();
|
let role_str = user_clone.role().to_string();
|
||||||
|
|
||||||
// Modificar el SQL para hacer un cast explícito al tipo auth.userrole
|
// Modify the SQL to do an explicit cast to the auth.userrole type
|
||||||
let _result = sqlx::query(
|
let _result = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO auth.users (
|
INSERT INTO auth.users (
|
||||||
@@ -87,7 +87,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
.bind(user_clone.username())
|
.bind(user_clone.username())
|
||||||
.bind(user_clone.email())
|
.bind(user_clone.email())
|
||||||
.bind(user_clone.password_hash())
|
.bind(user_clone.password_hash())
|
||||||
.bind(&role_str) // Convertir a string pero con cast explícito en SQL
|
.bind(&role_str) // Convert to string but with explicit cast in SQL
|
||||||
.bind(user_clone.storage_quota_bytes())
|
.bind(user_clone.storage_quota_bytes())
|
||||||
.bind(user_clone.storage_used_bytes())
|
.bind(user_clone.storage_used_bytes())
|
||||||
.bind(user_clone.created_at())
|
.bind(user_clone.created_at())
|
||||||
@@ -100,18 +100,18 @@ impl UserRepository for UserPgRepository {
|
|||||||
.await
|
.await
|
||||||
.map_err(Self::map_sqlx_error)?;
|
.map_err(Self::map_sqlx_error)?;
|
||||||
|
|
||||||
// Podríamos realizar operaciones adicionales aquí,
|
// We could perform additional operations here,
|
||||||
// como configurar permisos, roles, etc.
|
// such as configuring permissions, roles, etc.
|
||||||
|
|
||||||
Ok(user_clone)
|
Ok(user_clone)
|
||||||
}) as BoxFuture<'_, UserRepositoryResult<User>>
|
}) as BoxFuture<'_, UserRepositoryResult<User>>
|
||||||
}
|
}
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
Ok(user) // Devolvemos el usuario original por simplicidad
|
Ok(user) // Return the original user for simplicity
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene un usuario por ID
|
/// Gets a user by ID
|
||||||
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User> {
|
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -153,7 +153,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene un usuario por nombre de usuario
|
/// Gets a user by username
|
||||||
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User> {
|
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -195,7 +195,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene un usuario por correo electrónico
|
/// Gets a user by email
|
||||||
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User> {
|
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -237,9 +237,9 @@ impl UserRepository for UserPgRepository {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza un usuario existente utilizando una transacción
|
/// Updates an existing user using a transaction
|
||||||
async fn update_user(&self, user: User) -> UserRepositoryResult<User> {
|
async fn update_user(&self, user: User) -> UserRepositoryResult<User> {
|
||||||
// Creamos una copia del usuario para el closure
|
// Create a copy of the user for the closure
|
||||||
let user_clone = user.clone();
|
let user_clone = user.clone();
|
||||||
|
|
||||||
with_transaction(
|
with_transaction(
|
||||||
@@ -247,7 +247,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
"update_user",
|
"update_user",
|
||||||
|tx| {
|
|tx| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// Actualizar el usuario
|
// Update the user
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
UPDATE auth.users
|
UPDATE auth.users
|
||||||
@@ -278,8 +278,8 @@ impl UserRepository for UserPgRepository {
|
|||||||
.await
|
.await
|
||||||
.map_err(Self::map_sqlx_error)?;
|
.map_err(Self::map_sqlx_error)?;
|
||||||
|
|
||||||
// Podríamos realizar operaciones adicionales aquí dentro
|
// We could perform additional operations here inside
|
||||||
// de la misma transacción, como actualizar permisos, etc.
|
// the same transaction, such as updating permissions, etc.
|
||||||
|
|
||||||
Ok(user_clone)
|
Ok(user_clone)
|
||||||
}) as BoxFuture<'_, UserRepositoryResult<User>>
|
}) as BoxFuture<'_, UserRepositoryResult<User>>
|
||||||
@@ -289,7 +289,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza solo el uso de almacenamiento de un usuario
|
/// Updates only the storage usage of a user
|
||||||
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()> {
|
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -309,7 +309,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza la fecha de último inicio de sesión
|
/// Updates the last login date
|
||||||
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()> {
|
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -328,7 +328,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lista usuarios con paginación
|
/// Lists users with pagination
|
||||||
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>> {
|
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -378,7 +378,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
Ok(users)
|
Ok(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Activa o desactiva un usuario
|
/// Activates or deactivates a user
|
||||||
async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()> {
|
async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -398,7 +398,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cambia la contraseña de un usuario
|
/// Changes a user's password
|
||||||
async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()> {
|
async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -418,9 +418,9 @@ impl UserRepository for UserPgRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cambia el rol de un usuario
|
/// Changes a user's role
|
||||||
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()> {
|
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()> {
|
||||||
// Convertir el rol a string para el binding
|
// Convert the role to string for the binding
|
||||||
let role_str = role.to_string();
|
let role_str = role.to_string();
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -441,7 +441,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lista usuarios por rol
|
/// Lists users by role
|
||||||
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>> {
|
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -490,7 +490,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
Ok(users)
|
Ok(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina un usuario
|
/// Deletes a user
|
||||||
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> {
|
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -548,7 +548,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza la cuota de almacenamiento de un usuario
|
/// Updates a user's storage quota
|
||||||
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()> {
|
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -568,7 +568,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cuenta el número total de usuarios
|
/// Counts the total number of users
|
||||||
async fn count_users(&self) -> UserRepositoryResult<i64> {
|
async fn count_users(&self) -> UserRepositoryResult<i64> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
"SELECT COUNT(*) as count FROM auth.users"
|
"SELECT COUNT(*) as count FROM auth.users"
|
||||||
@@ -581,7 +581,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene estadísticas de almacenamiento agregadas
|
/// Gets aggregated storage statistics
|
||||||
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats> {
|
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -610,7 +610,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementación del puerto de almacenamiento para la capa de aplicación
|
// Storage port implementation for the application layer
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl UserStoragePort for UserPgRepository {
|
impl UserStoragePort for UserPgRepository {
|
||||||
async fn create_user(&self, user: User) -> Result<User, DomainError> {
|
async fn create_user(&self, user: User) -> Result<User, DomainError> {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Estructura para almacenar en el sistema de archivos
|
// Structure for storing in the file system
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct ShareRecord {
|
struct ShareRecord {
|
||||||
id: String,
|
id: String,
|
||||||
@@ -38,12 +38,12 @@ impl ShareFsRepository {
|
|||||||
Self { config }
|
Self { config }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene la ruta del archivo JSON donde se almacenan los enlaces compartidos
|
/// Gets the path to the JSON file where shared links are stored
|
||||||
fn get_shares_path(&self) -> String {
|
fn get_shares_path(&self) -> String {
|
||||||
format!("{}/shares.json", self.config.storage_path.display())
|
format!("{}/shares.json", self.config.storage_path.display())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lee todos los enlaces compartidos del archivo JSON
|
/// Reads all shared links from the JSON file
|
||||||
async fn read_shares(&self) -> Result<Vec<ShareRecord>, io::Error> {
|
async fn read_shares(&self) -> Result<Vec<ShareRecord>, io::Error> {
|
||||||
let path = self.get_shares_path();
|
let path = self.get_shares_path();
|
||||||
let path = Path::new(&path);
|
let path = Path::new(&path);
|
||||||
@@ -58,12 +58,12 @@ impl ShareFsRepository {
|
|||||||
Ok(shares)
|
Ok(shares)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Guarda todos los enlaces compartidos en el archivo JSON
|
/// Saves all shared links to the JSON file
|
||||||
async fn write_shares(&self, shares: &[ShareRecord]) -> Result<(), io::Error> {
|
async fn write_shares(&self, shares: &[ShareRecord]) -> Result<(), io::Error> {
|
||||||
let path = self.get_shares_path();
|
let path = self.get_shares_path();
|
||||||
let json = serde_json::to_string_pretty(shares)?;
|
let json = serde_json::to_string_pretty(shares)?;
|
||||||
|
|
||||||
// Asegúrate de que el directorio existe
|
// Make sure the directory exists
|
||||||
let dir = Path::new(&path).parent().unwrap();
|
let dir = Path::new(&path).parent().unwrap();
|
||||||
if !dir.exists() {
|
if !dir.exists() {
|
||||||
fs::create_dir_all(dir).await?
|
fs::create_dir_all(dir).await?
|
||||||
@@ -72,7 +72,7 @@ impl ShareFsRepository {
|
|||||||
fs::write(path, json).await
|
fs::write(path, json).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convierte un registro del sistema de archivos a una entidad de dominio
|
/// Converts a file system record to a domain entity
|
||||||
fn to_entity(&self, record: &ShareRecord) -> Share {
|
fn to_entity(&self, record: &ShareRecord) -> Share {
|
||||||
let item_type = ShareItemType::try_from(record.item_type.as_str())
|
let item_type = ShareItemType::try_from(record.item_type.as_str())
|
||||||
.unwrap_or(ShareItemType::File);
|
.unwrap_or(ShareItemType::File);
|
||||||
@@ -97,7 +97,7 @@ impl ShareFsRepository {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convierte una entidad de dominio a un registro para el sistema de archivos
|
/// Converts a domain entity to a file system record
|
||||||
fn to_record(&self, share: &Share) -> ShareRecord {
|
fn to_record(&self, share: &Share) -> ShareRecord {
|
||||||
ShareRecord {
|
ShareRecord {
|
||||||
id: share.id().to_string(),
|
id: share.id().to_string(),
|
||||||
@@ -122,16 +122,16 @@ impl ShareStoragePort for ShareFsRepository {
|
|||||||
let mut shares = self.read_shares().await
|
let mut shares = self.read_shares().await
|
||||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||||
|
|
||||||
// Verifica si el enlace ya existe
|
// Check if the link already exists
|
||||||
let existing_index = shares.iter().position(|s| s.id == share.id());
|
let existing_index = shares.iter().position(|s| s.id == share.id());
|
||||||
|
|
||||||
let record = self.to_record(share);
|
let record = self.to_record(share);
|
||||||
|
|
||||||
if let Some(index) = existing_index {
|
if let Some(index) = existing_index {
|
||||||
// Actualización
|
// Update
|
||||||
shares[index] = record;
|
shares[index] = record;
|
||||||
} else {
|
} else {
|
||||||
// Inserción
|
// Insert
|
||||||
shares.push(record);
|
shares.push(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,16 +190,16 @@ impl ShareStoragePort for ShareFsRepository {
|
|||||||
let mut shares = self.read_shares().await
|
let mut shares = self.read_shares().await
|
||||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||||
|
|
||||||
// Busca el índice del enlace a actualizar
|
// Find the index of the link to update
|
||||||
let index = shares.iter().position(|s| s.id == share.id())
|
let index = shares.iter().position(|s| s.id == share.id())
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
DomainError::not_found("Share", format!("Share with ID {} not found for update", share.id()))
|
DomainError::not_found("Share", format!("Share with ID {} not found for update", share.id()))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Actualiza el registro
|
// Update the record
|
||||||
shares[index] = self.to_record(share);
|
shares[index] = self.to_record(share);
|
||||||
|
|
||||||
// Guarda los cambios
|
// Save changes
|
||||||
self.write_shares(&shares).await
|
self.write_shares(&shares).await
|
||||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||||
|
|
||||||
@@ -210,16 +210,16 @@ impl ShareStoragePort for ShareFsRepository {
|
|||||||
let mut shares = self.read_shares().await
|
let mut shares = self.read_shares().await
|
||||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||||
|
|
||||||
// Encuentra el índice del enlace a eliminar
|
// Find the index of the link to delete
|
||||||
let initial_len = shares.len();
|
let initial_len = shares.len();
|
||||||
shares.retain(|s| s.id != id);
|
shares.retain(|s| s.id != id);
|
||||||
|
|
||||||
// Si no se eliminó ningún enlace, significa que no existía
|
// If no link was deleted, it means it didn't exist
|
||||||
if shares.len() == initial_len {
|
if shares.len() == initial_len {
|
||||||
return Err(DomainError::not_found("Share", format!("Share with ID {} not found for deletion", id)));
|
return Err(DomainError::not_found("Share", format!("Share with ID {} not found for deletion", id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guarda los cambios
|
// Save changes
|
||||||
self.write_shares(&shares).await
|
self.write_shares(&shares).await
|
||||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||||
|
|
||||||
@@ -230,15 +230,15 @@ impl ShareStoragePort for ShareFsRepository {
|
|||||||
let shares = self.read_shares().await
|
let shares = self.read_shares().await
|
||||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||||
|
|
||||||
// Filtra los enlaces del usuario
|
// Filter the user's links
|
||||||
let user_shares: Vec<ShareRecord> = shares.into_iter()
|
let user_shares: Vec<ShareRecord> = shares.into_iter()
|
||||||
.filter(|s| s.created_by == user_id)
|
.filter(|s| s.created_by == user_id)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Calcula el total
|
// Calculate the total
|
||||||
let total = user_shares.len();
|
let total = user_shares.len();
|
||||||
|
|
||||||
// Aplica la paginación
|
// Apply pagination
|
||||||
let paginated: Vec<Share> = user_shares.iter()
|
let paginated: Vec<Share> = user_shares.iter()
|
||||||
.skip(offset)
|
.skip(offset)
|
||||||
.take(limit)
|
.take(limit)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
|
|||||||
use crate::domain::repositories::trash_repository::TrashRepository;
|
use crate::domain::repositories::trash_repository::TrashRepository;
|
||||||
use crate::application::ports::outbound::IdMappingPort;
|
use crate::application::ports::outbound::IdMappingPort;
|
||||||
|
|
||||||
/// Estructura para almacenar elementos en la papelera en formato JSON
|
/// Structure for storing trash items in JSON format
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct TrashedItemEntry {
|
struct TrashedItemEntry {
|
||||||
id: String,
|
id: String,
|
||||||
@@ -25,7 +25,7 @@ struct TrashedItemEntry {
|
|||||||
deletion_date: String,
|
deletion_date: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementación del repositorio de papelera usando el sistema de archivos
|
/// Trash repository implementation using the file system
|
||||||
pub struct TrashFsRepository {
|
pub struct TrashFsRepository {
|
||||||
trash_dir: PathBuf,
|
trash_dir: PathBuf,
|
||||||
trash_index_path: PathBuf,
|
trash_index_path: PathBuf,
|
||||||
@@ -45,7 +45,7 @@ impl TrashFsRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asegura que existe el directorio de papelera
|
/// Ensures the trash directory exists
|
||||||
async fn ensure_trash_dir(&self) -> Result<()> {
|
async fn ensure_trash_dir(&self) -> Result<()> {
|
||||||
debug!("Checking if trash directory exists: {}", self.trash_dir.display());
|
debug!("Checking if trash directory exists: {}", self.trash_dir.display());
|
||||||
if !self.trash_dir.exists() {
|
if !self.trash_dir.exists() {
|
||||||
@@ -105,7 +105,7 @@ impl TrashFsRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene todas las entradas del índice de papelera
|
/// Gets all entries from the trash index
|
||||||
async fn get_trash_entries(&self) -> Result<Vec<TrashedItemEntry>> {
|
async fn get_trash_entries(&self) -> Result<Vec<TrashedItemEntry>> {
|
||||||
self.ensure_trash_dir().await?;
|
self.ensure_trash_dir().await?;
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ impl TrashFsRepository {
|
|||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Guarda todas las entradas en el índice de papelera
|
/// Saves all entries to the trash index
|
||||||
async fn save_trash_entries(&self, entries: Vec<TrashedItemEntry>) -> Result<()> {
|
async fn save_trash_entries(&self, entries: Vec<TrashedItemEntry>) -> Result<()> {
|
||||||
self.ensure_trash_dir().await?;
|
self.ensure_trash_dir().await?;
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ impl TrashFsRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convierte una entrada JSON a entidad TrashedItem
|
/// Converts a JSON entry to a TrashedItem entity
|
||||||
fn entry_to_trashed_item(&self, entry: TrashedItemEntry) -> Result<TrashedItem> {
|
fn entry_to_trashed_item(&self, entry: TrashedItemEntry) -> Result<TrashedItem> {
|
||||||
let item_type = match entry.item_type.as_str() {
|
let item_type = match entry.item_type.as_str() {
|
||||||
"file" => TrashedItemType::File,
|
"file" => TrashedItemType::File,
|
||||||
@@ -206,7 +206,7 @@ impl TrashFsRepository {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convierte una entidad TrashedItem a entrada JSON
|
/// Converts a TrashedItem entity to a JSON entry
|
||||||
fn trashed_item_to_entry(&self, item: &TrashedItem) -> TrashedItemEntry {
|
fn trashed_item_to_entry(&self, item: &TrashedItem) -> TrashedItemEntry {
|
||||||
TrashedItemEntry {
|
TrashedItemEntry {
|
||||||
id: item.id().to_string(),
|
id: item.id().to_string(),
|
||||||
@@ -228,9 +228,9 @@ impl TrashFsRepository {
|
|||||||
impl TrashRepository for TrashFsRepository {
|
impl TrashRepository for TrashFsRepository {
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn add_to_trash(&self, item: &TrashedItem) -> Result<()> {
|
async fn add_to_trash(&self, item: &TrashedItem) -> Result<()> {
|
||||||
debug!("Añadiendo elemento a la papelera: id={}, user={}", item.id(), item.user_id());
|
debug!("Adding item to trash: id={}, user={}", item.id(), item.user_id());
|
||||||
|
|
||||||
// Aseguramos que existe el directorio de la papelera para este usuario
|
// Ensure the trash directory exists for this user
|
||||||
let user_trash_dir = self.trash_dir.join("files").join(item.user_id().to_string());
|
let user_trash_dir = self.trash_dir.join("files").join(item.user_id().to_string());
|
||||||
debug!("User trash directory path: {}", user_trash_dir.display());
|
debug!("User trash directory path: {}", user_trash_dir.display());
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ impl TrashRepository for TrashFsRepository {
|
|||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> {
|
async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> {
|
||||||
debug!("Obteniendo elementos en papelera para usuario: {}", user_id);
|
debug!("Getting trash items for user: {}", user_id);
|
||||||
|
|
||||||
let entries = self.get_trash_entries().await?;
|
let entries = self.get_trash_entries().await?;
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ impl TrashRepository for TrashFsRepository {
|
|||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result<Option<TrashedItem>> {
|
async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result<Option<TrashedItem>> {
|
||||||
debug!("Buscando elemento en papelera: id={}, user={}", id, user_id);
|
debug!("Looking for item in trash: id={}, user={}", id, user_id);
|
||||||
|
|
||||||
let entries = self.get_trash_entries().await?;
|
let entries = self.get_trash_entries().await?;
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ impl TrashRepository for TrashFsRepository {
|
|||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
|
async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
|
||||||
debug!("Restaurando elemento de la papelera: id={}, user={}", id, user_id);
|
debug!("Restoring item from trash: id={}, user={}", id, user_id);
|
||||||
|
|
||||||
let mut entries = self.get_trash_entries().await?;
|
let mut entries = self.get_trash_entries().await?;
|
||||||
|
|
||||||
@@ -333,16 +333,16 @@ impl TrashRepository for TrashFsRepository {
|
|||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
|
async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
|
||||||
debug!("Eliminando permanentemente elemento de la papelera: id={}, user={}", id, user_id);
|
debug!("Permanently deleting item from trash: id={}, user={}", id, user_id);
|
||||||
|
|
||||||
// Simplemente eliminamos la entrada del índice
|
// Simply remove the entry from the index
|
||||||
// Los archivos físicos se eliminarán a través del repositorio correspondiente
|
// Physical files will be deleted through the corresponding repository
|
||||||
self.restore_from_trash(id, user_id).await
|
self.restore_from_trash(id, user_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn clear_trash(&self, user_id: &Uuid) -> Result<()> {
|
async fn clear_trash(&self, user_id: &Uuid) -> Result<()> {
|
||||||
debug!("Limpiando papelera para usuario: {}", user_id);
|
debug!("Clearing trash for user: {}", user_id);
|
||||||
|
|
||||||
let mut entries = self.get_trash_entries().await?;
|
let mut entries = self.get_trash_entries().await?;
|
||||||
let user_id_str = user_id.to_string();
|
let user_id_str = user_id.to_string();
|
||||||
@@ -355,7 +355,7 @@ impl TrashRepository for TrashFsRepository {
|
|||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> {
|
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> {
|
||||||
debug!("Buscando elementos de papelera expirados");
|
debug!("Looking for expired trash items");
|
||||||
|
|
||||||
let entries = self.get_trash_entries().await?;
|
let entries = self.get_trash_entries().await?;
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|||||||
@@ -5,71 +5,71 @@ use tokio::sync::{Mutex, Semaphore};
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
/// Tamaño por defecto de los buffers en el pool
|
/// Default buffer size in the pool
|
||||||
pub const DEFAULT_BUFFER_SIZE: usize = 64 * 1024; // 64KB
|
pub const DEFAULT_BUFFER_SIZE: usize = 64 * 1024; // 64KB
|
||||||
|
|
||||||
/// Número máximo por defecto de buffers en el pool
|
/// Default maximum number of buffers in the pool
|
||||||
pub const DEFAULT_MAX_BUFFERS: usize = 100;
|
pub const DEFAULT_MAX_BUFFERS: usize = 100;
|
||||||
|
|
||||||
/// Tiempo de vida por defecto de un buffer inactivo (en segundos)
|
/// Default time-to-live for an inactive buffer (in seconds)
|
||||||
pub const DEFAULT_BUFFER_TTL: u64 = 60;
|
pub const DEFAULT_BUFFER_TTL: u64 = 60;
|
||||||
|
|
||||||
/// Buffer pooling para optimizar operaciones de lectura/escritura
|
/// Buffer pooling to optimize read/write operations
|
||||||
pub struct BufferPool {
|
pub struct BufferPool {
|
||||||
/// Pool de buffers disponibles
|
/// Pool of available buffers
|
||||||
pool: Mutex<VecDeque<PooledBuffer>>,
|
pool: Mutex<VecDeque<PooledBuffer>>,
|
||||||
/// Semáforo para limitar el número máximo de buffers
|
/// Semaphore to limit the maximum number of buffers
|
||||||
limit: Semaphore,
|
limit: Semaphore,
|
||||||
/// Tamaño de los buffers en el pool
|
/// Size of buffers in the pool
|
||||||
buffer_size: usize,
|
buffer_size: usize,
|
||||||
/// Estadísticas del pool
|
/// Pool statistics
|
||||||
stats: Mutex<BufferPoolStats>,
|
stats: Mutex<BufferPoolStats>,
|
||||||
/// Tiempo de vida de un buffer inactivo
|
/// Time-to-live for an inactive buffer
|
||||||
buffer_ttl: Duration,
|
buffer_ttl: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Estructura para tracking de estadísticas del pool
|
/// Structure for tracking pool statistics
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct BufferPoolStats {
|
pub struct BufferPoolStats {
|
||||||
/// Número total de operaciones de get
|
/// Total number of get operations
|
||||||
pub gets: usize,
|
pub gets: usize,
|
||||||
/// Número de hits del pool (reutilización exitosa)
|
/// Number of pool hits (successful reuse)
|
||||||
pub hits: usize,
|
pub hits: usize,
|
||||||
/// Número de misses (creación de nuevo buffer)
|
/// Number of misses (new buffer creation)
|
||||||
pub misses: usize,
|
pub misses: usize,
|
||||||
/// Número de retornos al pool
|
/// Number of returns to the pool
|
||||||
pub returns: usize,
|
pub returns: usize,
|
||||||
/// Número de eviction por TTL
|
/// Number of TTL evictions
|
||||||
pub evictions: usize,
|
pub evictions: usize,
|
||||||
/// Número máximo de buffers alcanzado
|
/// Maximum number of buffers reached
|
||||||
pub max_buffers_reached: usize,
|
pub max_buffers_reached: usize,
|
||||||
/// Esperas por semáforo
|
/// Semaphore waits
|
||||||
pub waits: usize,
|
pub waits: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Buffer del pool con metadatos para gestión
|
/// Pool buffer with management metadata
|
||||||
struct PooledBuffer {
|
struct PooledBuffer {
|
||||||
/// Buffer real de bytes
|
/// Actual byte buffer
|
||||||
buffer: Vec<u8>,
|
buffer: Vec<u8>,
|
||||||
/// Timestamp de cuándo se añadió/retornó al pool
|
/// Timestamp of when it was added/returned to the pool
|
||||||
last_used: Instant,
|
last_used: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Buffer prestado del pool con cleanup automático
|
/// Borrowed buffer from the pool with automatic cleanup
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BorrowedBuffer {
|
pub struct BorrowedBuffer {
|
||||||
/// Buffer actual
|
/// Current buffer
|
||||||
buffer: Vec<u8>,
|
buffer: Vec<u8>,
|
||||||
/// Tamaño real utilizado del buffer
|
/// Actual used size of the buffer
|
||||||
used_size: usize,
|
used_size: usize,
|
||||||
/// Referencia al pool para retornar
|
/// Reference to the pool for returning
|
||||||
pool: Arc<BufferPool>,
|
pool: Arc<BufferPool>,
|
||||||
/// Si el buffer debe o no retornarse al pool
|
/// Whether the buffer should be returned to the pool or not
|
||||||
return_to_pool: bool,
|
return_to_pool: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BufferPool {
|
impl BufferPool {
|
||||||
/// Crea un nuevo pool de buffers
|
/// Creates a new buffer pool
|
||||||
pub fn new(buffer_size: usize, max_buffers: usize, buffer_ttl_secs: u64) -> Arc<Self> {
|
pub fn new(buffer_size: usize, max_buffers: usize, buffer_ttl_secs: u64) -> Arc<Self> {
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
pool: Mutex::new(VecDeque::with_capacity(max_buffers)),
|
pool: Mutex::new(VecDeque::with_capacity(max_buffers)),
|
||||||
@@ -80,7 +80,7 @@ impl BufferPool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un pool con configuración por defecto
|
/// Creates a pool with default configuration
|
||||||
pub fn default() -> Arc<Self> {
|
pub fn default() -> Arc<Self> {
|
||||||
Self::new(
|
Self::new(
|
||||||
DEFAULT_BUFFER_SIZE,
|
DEFAULT_BUFFER_SIZE,
|
||||||
@@ -89,25 +89,25 @@ impl BufferPool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene un buffer del pool o crea uno nuevo si es necesario.
|
/// Gets a buffer from the pool or creates a new one if needed.
|
||||||
/// This version takes an Arc<Self> to ensure the BorrowedBuffer keeps a proper
|
/// This version takes an Arc<Self> to ensure the BorrowedBuffer keeps a proper
|
||||||
/// reference to the shared pool (not a clone).
|
/// reference to the shared pool (not a clone).
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
pub async fn get_buffer(self: &Arc<Self>) -> BorrowedBuffer {
|
pub async fn get_buffer(self: &Arc<Self>) -> BorrowedBuffer {
|
||||||
// Incrementar contador de gets
|
// Increment get counter
|
||||||
{
|
{
|
||||||
let mut stats = self.stats.lock().await;
|
let mut stats = self.stats.lock().await;
|
||||||
stats.gets += 1;
|
stats.gets += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Control de concurrencia
|
// Concurrency control
|
||||||
// Acquire a semaphore permit. If none available, wait.
|
// Acquire a semaphore permit. If none available, wait.
|
||||||
// We forget() the permit so it doesn't auto-release on drop.
|
// We forget() the permit so it doesn't auto-release on drop.
|
||||||
// Instead, the permit is manually released in return_buffer/Drop via add_permits(1).
|
// Instead, the permit is manually released in return_buffer/Drop via add_permits(1).
|
||||||
match self.limit.try_acquire() {
|
match self.limit.try_acquire() {
|
||||||
Ok(permit) => permit.forget(),
|
Ok(permit) => permit.forget(),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// No hay permisos disponibles, esperamos
|
// No permits available, waiting
|
||||||
{
|
{
|
||||||
let mut stats = self.stats.lock().await;
|
let mut stats = self.stats.lock().await;
|
||||||
stats.waits += 1;
|
stats.waits += 1;
|
||||||
@@ -121,15 +121,15 @@ impl BufferPool {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Intentar obtener un buffer existente del pool
|
// Try to get an existing buffer from the pool
|
||||||
let mut pool_locked = self.pool.lock().await;
|
let mut pool_locked = self.pool.lock().await;
|
||||||
|
|
||||||
let pool_arc = Arc::clone(self);
|
let pool_arc = Arc::clone(self);
|
||||||
|
|
||||||
if let Some(mut pooled_buffer) = pool_locked.pop_front() {
|
if let Some(mut pooled_buffer) = pool_locked.pop_front() {
|
||||||
// Verificar si el buffer ha expirado
|
// Check if the buffer has expired
|
||||||
if pooled_buffer.last_used.elapsed() > self.buffer_ttl {
|
if pooled_buffer.last_used.elapsed() > self.buffer_ttl {
|
||||||
// Buffer expirado, descartamos y creamos uno nuevo
|
// Expired buffer, discard and create a new one
|
||||||
let mut stats = self.stats.lock().await;
|
let mut stats = self.stats.lock().await;
|
||||||
stats.evictions += 1;
|
stats.evictions += 1;
|
||||||
stats.misses += 1;
|
stats.misses += 1;
|
||||||
@@ -137,8 +137,8 @@ impl BufferPool {
|
|||||||
|
|
||||||
debug!("Buffer pool: evicted expired buffer");
|
debug!("Buffer pool: evicted expired buffer");
|
||||||
|
|
||||||
// Crear nuevo buffer (reutilizando el permiso)
|
// Create new buffer (reusing the permit)
|
||||||
drop(pool_locked); // Liberar el lock antes de retornar
|
drop(pool_locked); // Release the lock before returning
|
||||||
|
|
||||||
BorrowedBuffer {
|
BorrowedBuffer {
|
||||||
buffer: vec![0; self.buffer_size],
|
buffer: vec![0; self.buffer_size],
|
||||||
@@ -147,15 +147,15 @@ impl BufferPool {
|
|||||||
return_to_pool: true,
|
return_to_pool: true,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Buffer válido, lo reutilizamos
|
// Valid buffer, reuse it
|
||||||
let mut stats = self.stats.lock().await;
|
let mut stats = self.stats.lock().await;
|
||||||
stats.hits += 1;
|
stats.hits += 1;
|
||||||
drop(stats);
|
drop(stats);
|
||||||
|
|
||||||
// Liberar el lock antes de retornar
|
// Release the lock before returning
|
||||||
drop(pool_locked);
|
drop(pool_locked);
|
||||||
|
|
||||||
// Limpiar buffer por seguridad
|
// Clear buffer for security
|
||||||
pooled_buffer.buffer.fill(0);
|
pooled_buffer.buffer.fill(0);
|
||||||
|
|
||||||
BorrowedBuffer {
|
BorrowedBuffer {
|
||||||
@@ -166,12 +166,12 @@ impl BufferPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No hay buffers disponibles, creamos uno nuevo
|
// No buffers available, create a new one
|
||||||
let mut stats = self.stats.lock().await;
|
let mut stats = self.stats.lock().await;
|
||||||
stats.misses += 1;
|
stats.misses += 1;
|
||||||
drop(stats);
|
drop(stats);
|
||||||
|
|
||||||
// Liberar el lock antes de retornar
|
// Release the lock before returning
|
||||||
drop(pool_locked);
|
drop(pool_locked);
|
||||||
|
|
||||||
debug!("Buffer pool: creating new buffer");
|
debug!("Buffer pool: creating new buffer");
|
||||||
@@ -185,9 +185,9 @@ impl BufferPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retorna un buffer al pool
|
/// Returns a buffer to the pool
|
||||||
async fn return_buffer(&self, mut buffer: Vec<u8>) {
|
async fn return_buffer(&self, mut buffer: Vec<u8>) {
|
||||||
// Si el buffer es del tamaño incorrecto, lo descartamos
|
// If the buffer is the wrong size, discard it
|
||||||
if buffer.capacity() != self.buffer_size {
|
if buffer.capacity() != self.buffer_size {
|
||||||
debug!("Buffer pool: discarding buffer of wrong size: {} (expected {})",
|
debug!("Buffer pool: discarding buffer of wrong size: {} (expected {})",
|
||||||
buffer.capacity(), self.buffer_size);
|
buffer.capacity(), self.buffer_size);
|
||||||
@@ -196,10 +196,10 @@ impl BufferPool {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize para asegurar capacidad correcta
|
// Resize to ensure correct capacity
|
||||||
buffer.resize(self.buffer_size, 0);
|
buffer.resize(self.buffer_size, 0);
|
||||||
|
|
||||||
// Añadir al pool
|
// Add to the pool
|
||||||
let mut pool_locked = self.pool.lock().await;
|
let mut pool_locked = self.pool.lock().await;
|
||||||
|
|
||||||
pool_locked.push_back(PooledBuffer {
|
pool_locked.push_back(PooledBuffer {
|
||||||
@@ -207,7 +207,7 @@ impl BufferPool {
|
|||||||
last_used: Instant::now(),
|
last_used: Instant::now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
let mut stats = self.stats.lock().await;
|
let mut stats = self.stats.lock().await;
|
||||||
stats.returns += 1;
|
stats.returns += 1;
|
||||||
|
|
||||||
@@ -217,24 +217,24 @@ impl BufferPool {
|
|||||||
self.limit.add_permits(1);
|
self.limit.add_permits(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Limpia buffers expirados del pool
|
/// Cleans expired buffers from the pool
|
||||||
pub async fn clean_expired_buffers(&self) {
|
pub async fn clean_expired_buffers(&self) {
|
||||||
let _now = Instant::now();
|
let _now = Instant::now();
|
||||||
let mut pool_locked = self.pool.lock().await;
|
let mut pool_locked = self.pool.lock().await;
|
||||||
|
|
||||||
// Contar expirados
|
// Count expired
|
||||||
let count_before = pool_locked.len();
|
let count_before = pool_locked.len();
|
||||||
|
|
||||||
// Filtrar manteniendo solo los no expirados
|
// Filter keeping only non-expired
|
||||||
pool_locked.retain(|buffer| {
|
pool_locked.retain(|buffer| {
|
||||||
buffer.last_used.elapsed() <= self.buffer_ttl
|
buffer.last_used.elapsed() <= self.buffer_ttl
|
||||||
});
|
});
|
||||||
|
|
||||||
// Contar cuántos se eliminaron
|
// Count how many were removed
|
||||||
let removed = count_before - pool_locked.len();
|
let removed = count_before - pool_locked.len();
|
||||||
|
|
||||||
if removed > 0 {
|
if removed > 0 {
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
let mut stats = self.stats.lock().await;
|
let mut stats = self.stats.lock().await;
|
||||||
stats.evictions += removed;
|
stats.evictions += removed;
|
||||||
|
|
||||||
@@ -242,21 +242,21 @@ impl BufferPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene estadísticas actuales del pool
|
/// Gets current pool statistics
|
||||||
pub async fn get_stats(&self) -> BufferPoolStats {
|
pub async fn get_stats(&self) -> BufferPoolStats {
|
||||||
self.stats.lock().await.clone()
|
self.stats.lock().await.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inicia la tarea periódica de limpieza
|
/// Starts the periodic cleanup task
|
||||||
pub fn start_cleaner(pool: Arc<Self>) {
|
pub fn start_cleaner(pool: Arc<Self>) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let interval = Duration::from_secs(30); // Limpiar cada 30 segundos
|
let interval = Duration::from_secs(30); // Clean every 30 seconds
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(interval).await;
|
tokio::time::sleep(interval).await;
|
||||||
pool.clean_expired_buffers().await;
|
pool.clean_expired_buffers().await;
|
||||||
|
|
||||||
// Loguear estadísticas periódicamente
|
// Log statistics periodically
|
||||||
let stats = pool.get_stats().await;
|
let stats = pool.get_stats().await;
|
||||||
debug!("Buffer pool stats: gets={}, hits={}, misses={}, hit_ratio={:.2}%, returns={}, \
|
debug!("Buffer pool stats: gets={}, hits={}, misses={}, hit_ratio={:.2}%, returns={}, \
|
||||||
evictions={}, max_reached={}, waits={}",
|
evictions={}, max_reached={}, waits={}",
|
||||||
@@ -286,31 +286,31 @@ impl Clone for BufferPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BorrowedBuffer {
|
impl BorrowedBuffer {
|
||||||
/// Accede al buffer interno
|
/// Accesses the internal buffer
|
||||||
pub fn as_mut_slice(&mut self) -> &mut [u8] {
|
pub fn as_mut_slice(&mut self) -> &mut [u8] {
|
||||||
&mut self.buffer
|
&mut self.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene una referencia a los datos utilizados
|
/// Gets a reference to the used data
|
||||||
pub fn as_slice(&self) -> &[u8] {
|
pub fn as_slice(&self) -> &[u8] {
|
||||||
&self.buffer[..self.used_size]
|
&self.buffer[..self.used_size]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece cuántos bytes se utilizaron realmente
|
/// Sets how many bytes were actually used
|
||||||
pub fn set_used(&mut self, size: usize) {
|
pub fn set_used(&mut self, size: usize) {
|
||||||
self.used_size = min(size, self.buffer.len());
|
self.used_size = min(size, self.buffer.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convierte en un Vec<u8> que incluye solo los datos utilizados
|
/// Converts into a Vec<u8> that includes only the used data
|
||||||
pub fn into_vec(mut self) -> Vec<u8> {
|
pub fn into_vec(mut self) -> Vec<u8> {
|
||||||
// Marcar para no devolver al pool
|
// Mark to not return to pool
|
||||||
self.return_to_pool = false;
|
self.return_to_pool = false;
|
||||||
|
|
||||||
// Crear un nuevo vector solo con los datos utilizados
|
// Create a new vector with only the used data
|
||||||
self.buffer[..self.used_size].to_vec()
|
self.buffer[..self.used_size].to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copia datos a este buffer y actualiza el tamaño usado
|
/// Copies data to this buffer and updates the used size
|
||||||
pub fn copy_from_slice(&mut self, data: &[u8]) -> usize {
|
pub fn copy_from_slice(&mut self, data: &[u8]) -> usize {
|
||||||
let copy_size = min(data.len(), self.buffer.len());
|
let copy_size = min(data.len(), self.buffer.len());
|
||||||
self.buffer[..copy_size].copy_from_slice(&data[..copy_size]);
|
self.buffer[..copy_size].copy_from_slice(&data[..copy_size]);
|
||||||
@@ -318,32 +318,32 @@ impl BorrowedBuffer {
|
|||||||
copy_size
|
copy_size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Impide que el buffer se devuelva al pool al destruirse
|
/// Prevents the buffer from being returned to the pool on destruction
|
||||||
pub fn do_not_return(mut self) -> Self {
|
pub fn do_not_return(mut self) -> Self {
|
||||||
self.return_to_pool = false;
|
self.return_to_pool = false;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene el tamaño total del buffer
|
/// Gets the total buffer size
|
||||||
pub fn capacity(&self) -> usize {
|
pub fn capacity(&self) -> usize {
|
||||||
self.buffer.len()
|
self.buffer.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene el tamaño usado del buffer
|
/// Gets the used buffer size
|
||||||
pub fn used_size(&self) -> usize {
|
pub fn used_size(&self) -> usize {
|
||||||
self.used_size
|
self.used_size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cuando se hace drop de un BorrowedBuffer, lo devuelve al pool
|
// When a BorrowedBuffer is dropped, it is returned to the pool
|
||||||
impl Drop for BorrowedBuffer {
|
impl Drop for BorrowedBuffer {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if self.return_to_pool {
|
if self.return_to_pool {
|
||||||
// Tomar posesión del buffer y crear un clone del pool
|
// Take ownership of the buffer and create a clone of the pool
|
||||||
let buffer = std::mem::take(&mut self.buffer);
|
let buffer = std::mem::take(&mut self.buffer);
|
||||||
let pool = self.pool.clone();
|
let pool = self.pool.clone();
|
||||||
|
|
||||||
// Spawn del return para que el drop no bloquee
|
// Spawn the return so that drop doesn't block
|
||||||
// return_buffer will release the semaphore permit
|
// return_buffer will release the semaphore permit
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
pool.return_buffer(buffer).await;
|
pool.return_buffer(buffer).await;
|
||||||
@@ -361,39 +361,39 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_buffer_pooling() {
|
async fn test_buffer_pooling() {
|
||||||
// Crear pool pequeño para testing
|
// Create small pool for testing
|
||||||
let pool = BufferPool::new(1024, 5, 60);
|
let pool = BufferPool::new(1024, 5, 60);
|
||||||
|
|
||||||
// Obtener un buffer
|
// Get a buffer
|
||||||
let mut buffer1 = pool.get_buffer().await;
|
let mut buffer1 = pool.get_buffer().await;
|
||||||
buffer1.copy_from_slice(b"test data");
|
buffer1.copy_from_slice(b"test data");
|
||||||
assert_eq!(buffer1.as_slice(), b"test data");
|
assert_eq!(buffer1.as_slice(), b"test data");
|
||||||
|
|
||||||
// Obtener otro buffer
|
// Get another buffer
|
||||||
let buffer2 = pool.get_buffer().await;
|
let buffer2 = pool.get_buffer().await;
|
||||||
|
|
||||||
// Verificar stats
|
// Verify stats
|
||||||
let stats = pool.get_stats().await;
|
let stats = pool.get_stats().await;
|
||||||
assert_eq!(stats.gets, 2);
|
assert_eq!(stats.gets, 2);
|
||||||
assert_eq!(stats.hits, 0); // sin hits todavía
|
assert_eq!(stats.hits, 0); // no hits yet
|
||||||
assert_eq!(stats.misses, 2); // todos son misses
|
assert_eq!(stats.misses, 2); // all are misses
|
||||||
|
|
||||||
// Devolver buffer1 al pool (implícitamente por drop)
|
// Return buffer1 to pool (implicitly via drop)
|
||||||
drop(buffer1);
|
drop(buffer1);
|
||||||
|
|
||||||
// Permitir que el return asíncrono ocurra
|
// Allow the async return to occur
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
|
||||||
// Obtener otro buffer (debería reutilizar el retornado)
|
// Get another buffer (should reuse the returned one)
|
||||||
let buffer3 = pool.get_buffer().await;
|
let buffer3 = pool.get_buffer().await;
|
||||||
|
|
||||||
// Verificar stats actualizados
|
// Verify updated stats
|
||||||
let stats = pool.get_stats().await;
|
let stats = pool.get_stats().await;
|
||||||
assert_eq!(stats.gets, 3);
|
assert_eq!(stats.gets, 3);
|
||||||
assert_eq!(stats.hits, 1); // ahora debería haber un hit
|
assert_eq!(stats.hits, 1); // now there should be a hit
|
||||||
assert_eq!(stats.returns, 1); // un buffer retornado
|
assert_eq!(stats.returns, 1); // one buffer returned
|
||||||
|
|
||||||
// Limpiar
|
// Cleanup
|
||||||
drop(buffer2);
|
drop(buffer2);
|
||||||
drop(buffer3);
|
drop(buffer3);
|
||||||
}
|
}
|
||||||
@@ -402,19 +402,19 @@ mod tests {
|
|||||||
async fn test_buffer_operations() {
|
async fn test_buffer_operations() {
|
||||||
let pool = BufferPool::new(1024, 10, 60);
|
let pool = BufferPool::new(1024, 10, 60);
|
||||||
|
|
||||||
// Obtener buffer
|
// Get buffer
|
||||||
let mut buffer = pool.get_buffer().await;
|
let mut buffer = pool.get_buffer().await;
|
||||||
|
|
||||||
// Escribir datos
|
// Write data
|
||||||
buffer.copy_from_slice(b"Hello, world!");
|
buffer.copy_from_slice(b"Hello, world!");
|
||||||
assert_eq!(buffer.used_size(), 13);
|
assert_eq!(buffer.used_size(), 13);
|
||||||
assert_eq!(buffer.as_slice(), b"Hello, world!");
|
assert_eq!(buffer.as_slice(), b"Hello, world!");
|
||||||
|
|
||||||
// Convertir a vec y verificar
|
// Convert to vec and verify
|
||||||
let vec = buffer.into_vec(); // Esto impide retornar al pool
|
let vec = buffer.into_vec(); // This prevents returning to pool
|
||||||
assert_eq!(vec, b"Hello, world!");
|
assert_eq!(vec, b"Hello, world!");
|
||||||
|
|
||||||
// Verificar que no se incrementan los returns (buffer no retornado)
|
// Verify that returns are not incremented (buffer not returned)
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
let stats = pool.get_stats().await;
|
let stats = pool.get_stats().await;
|
||||||
assert_eq!(stats.returns, 0);
|
assert_eq!(stats.returns, 0);
|
||||||
@@ -422,76 +422,76 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_pool_limit() {
|
async fn test_pool_limit() {
|
||||||
// Pool con solo 3 buffers
|
// Pool with only 3 buffers
|
||||||
let pool = BufferPool::new(1024, 3, 60);
|
let pool = BufferPool::new(1024, 3, 60);
|
||||||
|
|
||||||
// Obtener 3 buffers (alcanza el límite)
|
// Get 3 buffers (reaches the limit)
|
||||||
let buffer1 = pool.get_buffer().await;
|
let buffer1 = pool.get_buffer().await;
|
||||||
let buffer2 = pool.get_buffer().await;
|
let buffer2 = pool.get_buffer().await;
|
||||||
let buffer3 = pool.get_buffer().await;
|
let buffer3 = pool.get_buffer().await;
|
||||||
|
|
||||||
// Verificar stats
|
// Verify stats
|
||||||
let stats = pool.get_stats().await;
|
let stats = pool.get_stats().await;
|
||||||
assert_eq!(stats.gets, 3);
|
assert_eq!(stats.gets, 3);
|
||||||
assert_eq!(stats.waits, 0); // sin esperas todavía
|
assert_eq!(stats.waits, 0); // no waits yet
|
||||||
|
|
||||||
// Intentar obtener un 4º buffer en una tarea separada (debería esperar)
|
// Try to get a 4th buffer in a separate task (should wait)
|
||||||
let pool_clone = pool.clone();
|
let pool_clone = pool.clone();
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
let _buffer4 = pool_clone.get_buffer().await;
|
let _buffer4 = pool_clone.get_buffer().await;
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dar tiempo para que la tarea intente tomar el buffer
|
// Give time for the task to try to take the buffer
|
||||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
// Verificar que hay una espera
|
// Verify there is a wait
|
||||||
let stats = pool.get_stats().await;
|
let stats = pool.get_stats().await;
|
||||||
assert_eq!(stats.waits, 1);
|
assert_eq!(stats.waits, 1);
|
||||||
|
|
||||||
// Liberar un buffer
|
// Release a buffer
|
||||||
drop(buffer1);
|
drop(buffer1);
|
||||||
|
|
||||||
// Dar tiempo para el retorno asíncrono y para que la tarea en espera obtenga su buffer
|
// Give time for the async return and for the waiting task to get its buffer
|
||||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
// Verificar que la tarea pudo continuar
|
// Verify the task was able to continue
|
||||||
assert!(handle.await.unwrap());
|
assert!(handle.await.unwrap());
|
||||||
|
|
||||||
// Limpiar
|
// Cleanup
|
||||||
drop(buffer2);
|
drop(buffer2);
|
||||||
drop(buffer3);
|
drop(buffer3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_ttl_expiration() {
|
async fn test_ttl_expiration() {
|
||||||
// Pool con TTL muy corto para testing
|
// Pool with very short TTL for testing
|
||||||
let pool = BufferPool::new(1024, 5, 1); // 1 segundo TTL
|
let pool = BufferPool::new(1024, 5, 1); // 1 second TTL
|
||||||
|
|
||||||
// Obtener y devolver un buffer
|
// Get and return a buffer
|
||||||
let buffer = pool.get_buffer().await;
|
let buffer = pool.get_buffer().await;
|
||||||
drop(buffer);
|
drop(buffer);
|
||||||
|
|
||||||
// Permitir que el return asíncrono ocurra
|
// Allow the async return to occur
|
||||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
// Verificar que hay un buffer en el pool
|
// Verify there is a buffer in the pool
|
||||||
let stats = pool.get_stats().await;
|
let stats = pool.get_stats().await;
|
||||||
assert_eq!(stats.returns, 1);
|
assert_eq!(stats.returns, 1);
|
||||||
|
|
||||||
// Esperar a que expire el TTL
|
// Wait for the TTL to expire
|
||||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
// Limpiar expirados
|
// Clean expired
|
||||||
pool.clean_expired_buffers().await;
|
pool.clean_expired_buffers().await;
|
||||||
|
|
||||||
// Obtener otro buffer (debería ser un miss ya que el anterior expiró)
|
// Get another buffer (should be a miss since the previous one expired)
|
||||||
let _buffer2 = pool.get_buffer().await;
|
let _buffer2 = pool.get_buffer().await;
|
||||||
|
|
||||||
// Verificar stats
|
// Verify stats
|
||||||
let stats = pool.get_stats().await;
|
let stats = pool.get_stats().await;
|
||||||
assert_eq!(stats.evictions, 1); // un buffer expirado
|
assert_eq!(stats.evictions, 1); // one expired buffer
|
||||||
assert_eq!(stats.hits, 0); // sin hits (el buffer expiró)
|
assert_eq!(stats.hits, 0); // no hits (the buffer expired)
|
||||||
assert_eq!(stats.misses, 2); // dos misses (1er y 3er get)
|
assert_eq!(stats.misses, 2); // two misses (1st and 3rd get)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,16 +16,16 @@ use crate::application::ports::compression_ports::{
|
|||||||
use crate::domain::errors::DomainError;
|
use crate::domain::errors::DomainError;
|
||||||
use crate::infrastructure::services::buffer_pool::BufferPool;
|
use crate::infrastructure::services::buffer_pool::BufferPool;
|
||||||
|
|
||||||
/// Nivel de compresión para ficheros
|
/// Compression level for files
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum CompressionLevel {
|
pub enum CompressionLevel {
|
||||||
/// Sin compresión (solo para transferencia)
|
/// No compression (transfer only)
|
||||||
None = 0,
|
None = 0,
|
||||||
/// Compresión rápida con menor ratio
|
/// Fast compression with lower ratio
|
||||||
Fast = 1,
|
Fast = 1,
|
||||||
/// Compresión balanceada (por defecto)
|
/// Balanced compression (default)
|
||||||
Default = 6,
|
Default = 6,
|
||||||
/// Compresión máxima (más lenta)
|
/// Maximum compression (slower)
|
||||||
Best = 9,
|
Best = 9,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,49 +40,49 @@ impl From<CompressionLevel> for Compression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Umbral de tamaño para decidir si se comprime o no
|
/// Size threshold to decide whether to compress or not
|
||||||
const COMPRESSION_SIZE_THRESHOLD: u64 = 1024 * 50; // 50KB
|
const COMPRESSION_SIZE_THRESHOLD: u64 = 1024 * 50; // 50KB
|
||||||
|
|
||||||
/// Interfaz para servicios de compresión
|
/// Interface for compression services
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait CompressionService: Send + Sync {
|
pub trait CompressionService: Send + Sync {
|
||||||
/// Comprime datos en memoria
|
/// Compresses data in memory
|
||||||
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>>;
|
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>>;
|
||||||
|
|
||||||
/// Descomprime datos en memoria
|
/// Decompresses data in memory
|
||||||
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>>;
|
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>>;
|
||||||
|
|
||||||
/// Comprime un stream de datos
|
/// Compresses a data stream
|
||||||
fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
|
fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
|
||||||
-> impl Stream<Item = io::Result<Bytes>> + Send
|
-> impl Stream<Item = io::Result<Bytes>> + Send
|
||||||
where
|
where
|
||||||
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
|
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
|
||||||
|
|
||||||
/// Descomprime un stream de datos
|
/// Decompresses a data stream
|
||||||
fn decompress_stream<S>(&self, compressed_stream: S)
|
fn decompress_stream<S>(&self, compressed_stream: S)
|
||||||
-> impl Stream<Item = io::Result<Bytes>> + Send
|
-> impl Stream<Item = io::Result<Bytes>> + Send
|
||||||
where
|
where
|
||||||
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
|
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
|
||||||
|
|
||||||
/// Determina si un archivo debe ser comprimido basado en su tipo MIME y tamaño
|
/// Determines whether a file should be compressed based on its MIME type and size
|
||||||
fn should_compress(&self, mime_type: &str, size: u64) -> bool;
|
fn should_compress(&self, mime_type: &str, size: u64) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementación de servicios de compresión usando Gzip
|
/// Gzip compression service implementation
|
||||||
pub struct GzipCompressionService {
|
pub struct GzipCompressionService {
|
||||||
/// Pool de buffers para optimización de memoria
|
/// Buffer pool for memory optimization
|
||||||
buffer_pool: Option<Arc<BufferPool>>,
|
buffer_pool: Option<Arc<BufferPool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GzipCompressionService {
|
impl GzipCompressionService {
|
||||||
/// Crea una nueva instancia del servicio
|
/// Creates a new service instance
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffer_pool: None,
|
buffer_pool: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una nueva instancia del servicio con buffer pool
|
/// Creates a new service instance with buffer pool
|
||||||
pub fn new_with_buffer_pool(buffer_pool: Arc<BufferPool>) -> Self {
|
pub fn new_with_buffer_pool(buffer_pool: Arc<BufferPool>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffer_pool: Some(buffer_pool),
|
buffer_pool: Some(buffer_pool),
|
||||||
@@ -92,64 +92,64 @@ impl GzipCompressionService {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CompressionService for GzipCompressionService {
|
impl CompressionService for GzipCompressionService {
|
||||||
/// Comprime datos en memoria usando Gzip
|
/// Compresses data in memory using Gzip
|
||||||
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>> {
|
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>> {
|
||||||
// Si tenemos un buffer pool, usar un buffer prestado para la compresión
|
// If we have a buffer pool, use a borrowed buffer for compression
|
||||||
if let Some(pool) = &self.buffer_pool {
|
if let Some(pool) = &self.buffer_pool {
|
||||||
// Estimar el tamaño de la compresión (aproximadamente 80% del original para casos típicos)
|
// Estimate the compression size (approximately 80% of original for typical cases)
|
||||||
let estimated_size = (data.len() as f64 * 0.8) as usize;
|
let estimated_size = (data.len() as f64 * 0.8) as usize;
|
||||||
|
|
||||||
// Obtener un buffer del pool
|
// Get a buffer from the pool
|
||||||
let buffer = pool.get_buffer().await;
|
let buffer = pool.get_buffer().await;
|
||||||
|
|
||||||
// Comprobar si el buffer es suficientemente grande
|
// Check if the buffer is large enough
|
||||||
if buffer.capacity() >= estimated_size {
|
if buffer.capacity() >= estimated_size {
|
||||||
// Ejecutar la compresión en un worker thread usando el buffer
|
// Run compression in a worker thread using the buffer
|
||||||
let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
|
let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
|
||||||
let buffer_clone = buffer_ptr.clone();
|
let buffer_clone = buffer_ptr.clone();
|
||||||
|
|
||||||
// Comprimir datos
|
// Compress data
|
||||||
// Clonar los datos para evitar problemas de lifetime
|
// Clone the data to avoid lifetime issues
|
||||||
let data_owned = data.to_vec();
|
let data_owned = data.to_vec();
|
||||||
|
|
||||||
let result = tokio::task::spawn_blocking(move || {
|
let result = tokio::task::spawn_blocking(move || {
|
||||||
let mut encoder = GzEncoderRead::new(&data_owned[..], level.into());
|
let mut encoder = GzEncoderRead::new(&data_owned[..], level.into());
|
||||||
|
|
||||||
// Intentar bloquear el mutex (no debería fallar ya que estamos en un hilo separado)
|
// Try to lock the mutex (should not fail since we are in a separate thread)
|
||||||
let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
|
let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
|
||||||
buffer => buffer,
|
buffer => buffer,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Leer directamente en el buffer
|
// Read directly into the buffer
|
||||||
let read_bytes = encoder.read(buffer_guard.as_mut_slice())?;
|
let read_bytes = encoder.read(buffer_guard.as_mut_slice())?;
|
||||||
buffer_guard.set_used(read_bytes);
|
buffer_guard.set_used(read_bytes);
|
||||||
|
|
||||||
Ok(()) as io::Result<()>
|
Ok(()) as io::Result<()>
|
||||||
}).await;
|
}).await;
|
||||||
|
|
||||||
// Verificar resultado
|
// Verify result
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(())) => {
|
Ok(Ok(())) => {
|
||||||
// Obtener el buffer y convertirlo a Vec<u8>
|
// Get the buffer and convert it to Vec<u8>
|
||||||
let buffer = buffer_ptr.lock().await;
|
let buffer = buffer_ptr.lock().await;
|
||||||
let cloned_buffer = buffer.clone();
|
let cloned_buffer = buffer.clone();
|
||||||
drop(buffer); // Liberar el mutex primero
|
drop(buffer); // Release the mutex first
|
||||||
return Ok(cloned_buffer.into_vec());
|
return Ok(cloned_buffer.into_vec());
|
||||||
},
|
},
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
error!("Error en compresión con buffer pool: {}", e);
|
error!("Compression error with buffer pool: {}", e);
|
||||||
// Continuar con implementación estándar
|
// Fall back to standard implementation
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error en task de compresión con buffer pool: {}", e);
|
error!("Compression task error with buffer pool: {}", e);
|
||||||
// Continuar con implementación estándar
|
// Fall back to standard implementation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementación estándar si no hay buffer pool o el buffer es insuficiente
|
// Standard implementation if there is no buffer pool or the buffer is insufficient
|
||||||
// Clonar los datos para evitar problemas de lifetime
|
// Clone the data to avoid lifetime issues
|
||||||
let data_owned = data.to_vec();
|
let data_owned = data.to_vec();
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
@@ -158,79 +158,79 @@ impl CompressionService for GzipCompressionService {
|
|||||||
encoder.read_to_end(&mut compressed)?;
|
encoder.read_to_end(&mut compressed)?;
|
||||||
Ok(compressed)
|
Ok(compressed)
|
||||||
}).await.unwrap_or_else(|e| {
|
}).await.unwrap_or_else(|e| {
|
||||||
error!("Error en task de compresión: {}", e);
|
error!("Compression task error: {}", e);
|
||||||
Err(io::Error::new(io::ErrorKind::Other, e.to_string()))
|
Err(io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Descomprime datos en memoria
|
/// Decompresses data in memory
|
||||||
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>> {
|
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>> {
|
||||||
// Si tenemos un buffer pool, usar un buffer prestado para la descompresión
|
// If we have a buffer pool, use a borrowed buffer for decompression
|
||||||
if let Some(pool) = &self.buffer_pool {
|
if let Some(pool) = &self.buffer_pool {
|
||||||
// Estimar el tamaño de la descompresión (aproximadamente 5x del comprimido para casos típicos)
|
// Estimate the decompression size (approximately 5x of compressed for typical cases)
|
||||||
let estimated_size = compressed_data.len() * 5;
|
let estimated_size = compressed_data.len() * 5;
|
||||||
|
|
||||||
// Obtener un buffer del pool
|
// Get a buffer from the pool
|
||||||
let buffer = pool.get_buffer().await;
|
let buffer = pool.get_buffer().await;
|
||||||
|
|
||||||
// Comprobar si el buffer es suficientemente grande
|
// Check if the buffer is large enough
|
||||||
if buffer.capacity() >= estimated_size {
|
if buffer.capacity() >= estimated_size {
|
||||||
// Clonar datos comprimidos para mover al worker
|
// Clone compressed data to move to the worker
|
||||||
let data = compressed_data.to_vec();
|
let data = compressed_data.to_vec();
|
||||||
let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
|
let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
|
||||||
let buffer_clone = buffer_ptr.clone();
|
let buffer_clone = buffer_ptr.clone();
|
||||||
|
|
||||||
// Descomprimir datos
|
// Decompress data
|
||||||
let result = tokio::task::spawn_blocking(move || {
|
let result = tokio::task::spawn_blocking(move || {
|
||||||
let mut decoder = GzDecoder::new(&data[..]);
|
let mut decoder = GzDecoder::new(&data[..]);
|
||||||
|
|
||||||
// Intentar bloquear el mutex
|
// Try to lock the mutex
|
||||||
let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
|
let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
|
||||||
buffer => buffer,
|
buffer => buffer,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Leer directamente en el buffer
|
// Read directly into the buffer
|
||||||
let read_bytes = decoder.read(buffer_guard.as_mut_slice())?;
|
let read_bytes = decoder.read(buffer_guard.as_mut_slice())?;
|
||||||
buffer_guard.set_used(read_bytes);
|
buffer_guard.set_used(read_bytes);
|
||||||
|
|
||||||
Ok(()) as io::Result<()>
|
Ok(()) as io::Result<()>
|
||||||
}).await;
|
}).await;
|
||||||
|
|
||||||
// Verificar resultado
|
// Verify result
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(())) => {
|
Ok(Ok(())) => {
|
||||||
// Obtener el buffer y convertirlo a Vec<u8>
|
// Get the buffer and convert it to Vec<u8>
|
||||||
let buffer = buffer_ptr.lock().await;
|
let buffer = buffer_ptr.lock().await;
|
||||||
let cloned_buffer = buffer.clone();
|
let cloned_buffer = buffer.clone();
|
||||||
drop(buffer); // Liberar el mutex primero
|
drop(buffer); // Release the mutex first
|
||||||
return Ok(cloned_buffer.into_vec());
|
return Ok(cloned_buffer.into_vec());
|
||||||
},
|
},
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
error!("Error en descompresión con buffer pool: {}", e);
|
error!("Decompression error with buffer pool: {}", e);
|
||||||
// Continuar con implementación estándar
|
// Fall back to standard implementation
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error en task de descompresión con buffer pool: {}", e);
|
error!("Decompression task error with buffer pool: {}", e);
|
||||||
// Continuar con implementación estándar
|
// Fall back to standard implementation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementación estándar si no hay buffer pool o el buffer es insuficiente
|
// Standard implementation if there is no buffer pool or the buffer is insufficient
|
||||||
let data = compressed_data.to_vec(); // Clonar para mover al worker
|
let data = compressed_data.to_vec(); // Clone to move to the worker
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let mut decoder = GzDecoder::new(&data[..]);
|
let mut decoder = GzDecoder::new(&data[..]);
|
||||||
let mut decompressed = Vec::new();
|
let mut decompressed = Vec::new();
|
||||||
decoder.read_to_end(&mut decompressed)?;
|
decoder.read_to_end(&mut decompressed)?;
|
||||||
Ok(decompressed)
|
Ok(decompressed)
|
||||||
}).await.unwrap_or_else(|e| {
|
}).await.unwrap_or_else(|e| {
|
||||||
error!("Error en task de descompresión: {}", e);
|
error!("Decompression task error: {}", e);
|
||||||
Err(io::Error::new(io::ErrorKind::Other, e.to_string()))
|
Err(io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Comprime un stream de bytes
|
/// Compresses a byte stream
|
||||||
fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
|
fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
|
||||||
-> impl Stream<Item = io::Result<Bytes>> + Send
|
-> impl Stream<Item = io::Result<Bytes>> + Send
|
||||||
where
|
where
|
||||||
@@ -271,7 +271,7 @@ impl CompressionService for GzipCompressionService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Descomprime un stream de bytes
|
/// Decompresses a byte stream
|
||||||
fn decompress_stream<S>(&self, compressed_stream: S)
|
fn decompress_stream<S>(&self, compressed_stream: S)
|
||||||
-> impl Stream<Item = io::Result<Bytes>> + Send
|
-> impl Stream<Item = io::Result<Bytes>> + Send
|
||||||
where
|
where
|
||||||
@@ -310,14 +310,14 @@ impl CompressionService for GzipCompressionService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determina si un archivo debe ser comprimido basado en su tipo MIME y tamaño
|
/// Determines whether a file should be compressed based on its MIME type and size
|
||||||
fn should_compress(&self, mime_type: &str, size: u64) -> bool {
|
fn should_compress(&self, mime_type: &str, size: u64) -> bool {
|
||||||
// No comprimir archivos muy pequeños (overhead)
|
// Do not compress very small files (overhead)
|
||||||
if size < COMPRESSION_SIZE_THRESHOLD {
|
if size < COMPRESSION_SIZE_THRESHOLD {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No comprimir archivos ya comprimidos
|
// Do not compress already compressed files
|
||||||
if mime_type.starts_with("image/")
|
if mime_type.starts_with("image/")
|
||||||
&& !mime_type.contains("svg")
|
&& !mime_type.contains("svg")
|
||||||
&& !mime_type.contains("bmp") {
|
&& !mime_type.contains("bmp") {
|
||||||
@@ -345,7 +345,7 @@ impl CompressionService for GzipCompressionService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comprimir archivos de texto, documentos, y otros tipos compresibles
|
// Compress text files, documents, and other compressible types
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,19 +389,19 @@ mod tests {
|
|||||||
async fn test_compress_decompress_data() {
|
async fn test_compress_decompress_data() {
|
||||||
let service = GzipCompressionService::new();
|
let service = GzipCompressionService::new();
|
||||||
|
|
||||||
// Datos de prueba
|
// Test data
|
||||||
let data = "Hello, world! ".repeat(1000).into_bytes();
|
let data = "Hello, world! ".repeat(1000).into_bytes();
|
||||||
|
|
||||||
// Comprimir
|
// Compress
|
||||||
let compressed = CompressionService::compress_data(&service, &data, CompressionLevel::Default).await.unwrap();
|
let compressed = CompressionService::compress_data(&service, &data, CompressionLevel::Default).await.unwrap();
|
||||||
|
|
||||||
// Verificar que la compresión reduce el tamaño
|
// Verify that compression reduces the size
|
||||||
assert!(compressed.len() < data.len());
|
assert!(compressed.len() < data.len());
|
||||||
|
|
||||||
// Descomprimir
|
// Decompress
|
||||||
let decompressed = CompressionService::decompress_data(&service, &compressed).await.unwrap();
|
let decompressed = CompressionService::decompress_data(&service, &compressed).await.unwrap();
|
||||||
|
|
||||||
// Verificar que los datos originales se recuperan correctamente
|
// Verify that the original data is recovered correctly
|
||||||
assert_eq!(decompressed, data);
|
assert_eq!(decompressed, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,30 +409,30 @@ mod tests {
|
|||||||
async fn test_compress_decompress_stream() {
|
async fn test_compress_decompress_stream() {
|
||||||
let service = GzipCompressionService::new();
|
let service = GzipCompressionService::new();
|
||||||
|
|
||||||
// Crear datos de prueba
|
// Create test data
|
||||||
let chunks = vec![
|
let chunks = vec![
|
||||||
Ok(Bytes::from("Hello, ")),
|
Ok(Bytes::from("Hello, ")),
|
||||||
Ok(Bytes::from("world! ")),
|
Ok(Bytes::from("world! ")),
|
||||||
Ok(Bytes::from("This is a test of streaming compression.")),
|
Ok(Bytes::from("This is a test of streaming compression.")),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Convertir a stream
|
// Convert to stream
|
||||||
let input_stream = futures::stream::iter(chunks);
|
let input_stream = futures::stream::iter(chunks);
|
||||||
|
|
||||||
// Comprimir el stream
|
// Compress the stream
|
||||||
let compressed_stream = service.compress_stream(input_stream, CompressionLevel::Default);
|
let compressed_stream = service.compress_stream(input_stream, CompressionLevel::Default);
|
||||||
|
|
||||||
// Recolectar los bytes comprimidos
|
// Collect the compressed bytes
|
||||||
let compressed_bytes = compressed_stream
|
let compressed_bytes = compressed_stream
|
||||||
.try_fold(Vec::new(), |mut acc, chunk| async move {
|
.try_fold(Vec::new(), |mut acc, chunk| async move {
|
||||||
acc.extend_from_slice(&chunk);
|
acc.extend_from_slice(&chunk);
|
||||||
Ok(acc)
|
Ok(acc)
|
||||||
}).await.unwrap();
|
}).await.unwrap();
|
||||||
|
|
||||||
// Descomprimir los datos
|
// Decompress the data
|
||||||
let decompressed = CompressionService::decompress_data(&service, &compressed_bytes).await.unwrap();
|
let decompressed = CompressionService::decompress_data(&service, &compressed_bytes).await.unwrap();
|
||||||
|
|
||||||
// Verificar resultado
|
// Verify result
|
||||||
let expected = "Hello, world! This is a test of streaming compression.";
|
let expected = "Hello, world! This is a test of streaming compression.";
|
||||||
assert_eq!(String::from_utf8(decompressed).unwrap(), expected);
|
assert_eq!(String::from_utf8(decompressed).unwrap(), expected);
|
||||||
}
|
}
|
||||||
@@ -441,17 +441,17 @@ mod tests {
|
|||||||
fn test_should_compress() {
|
fn test_should_compress() {
|
||||||
let service = GzipCompressionService::new();
|
let service = GzipCompressionService::new();
|
||||||
|
|
||||||
// Casos que no deberían comprimirse
|
// Cases that should not be compressed
|
||||||
assert!(!CompressionService::should_compress(&service, "image/jpeg", 100 * 1024));
|
assert!(!CompressionService::should_compress(&service, "image/jpeg", 100 * 1024));
|
||||||
assert!(!CompressionService::should_compress(&service, "video/mp4", 10 * 1024 * 1024));
|
assert!(!CompressionService::should_compress(&service, "video/mp4", 10 * 1024 * 1024));
|
||||||
assert!(!CompressionService::should_compress(&service, "application/zip", 5 * 1024 * 1024));
|
assert!(!CompressionService::should_compress(&service, "application/zip", 5 * 1024 * 1024));
|
||||||
|
|
||||||
// Casos que sí deberían comprimirse
|
// Cases that should be compressed
|
||||||
assert!(CompressionService::should_compress(&service, "text/html", 100 * 1024));
|
assert!(CompressionService::should_compress(&service, "text/html", 100 * 1024));
|
||||||
assert!(CompressionService::should_compress(&service, "application/json", 200 * 1024));
|
assert!(CompressionService::should_compress(&service, "application/json", 200 * 1024));
|
||||||
assert!(CompressionService::should_compress(&service, "text/plain", 1024 * 1024));
|
assert!(CompressionService::should_compress(&service, "text/plain", 1024 * 1024));
|
||||||
|
|
||||||
// Archivos pequeños no deberían comprimirse independientemente del tipo
|
// Small files should not be compressed regardless of type
|
||||||
assert!(!CompressionService::should_compress(&service, "text/html", 10 * 1024));
|
assert!(!CompressionService::should_compress(&service, "text/html", 10 * 1024));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,61 +13,61 @@ use crate::domain::entities::file::File;
|
|||||||
|
|
||||||
use crate::common::config::AppConfig;
|
use crate::common::config::AppConfig;
|
||||||
|
|
||||||
/// Tipos de entradas en caché
|
/// Cache entry types
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum CacheEntryType {
|
pub enum CacheEntryType {
|
||||||
/// Archivo
|
/// File
|
||||||
File,
|
File,
|
||||||
/// Directorio
|
/// Directory
|
||||||
Directory,
|
Directory,
|
||||||
/// Tipo desconocido
|
/// Unknown type
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Estadísticas de caché para monitoreo
|
/// Cache statistics for monitoring
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct CacheStats {
|
pub struct CacheStats {
|
||||||
/// Número de hits en caché
|
/// Number of cache hits
|
||||||
pub hits: usize,
|
pub hits: usize,
|
||||||
/// Número de misses en caché
|
/// Number of cache misses
|
||||||
pub misses: usize,
|
pub misses: usize,
|
||||||
/// Número de invalidaciones manuales
|
/// Number of manual invalidations
|
||||||
pub invalidations: usize,
|
pub invalidations: usize,
|
||||||
/// Número de expiraciones automáticas
|
/// Number of automatic expirations
|
||||||
pub expirations: usize,
|
pub expirations: usize,
|
||||||
/// Número de inserciones en caché
|
/// Number of cache inserts
|
||||||
pub inserts: usize,
|
pub inserts: usize,
|
||||||
/// Tiempo total ahorrado (milisegundos)
|
/// Total time saved (milliseconds)
|
||||||
pub time_saved_ms: u64,
|
pub time_saved_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Metadatos completos de archivo en caché
|
/// Complete cached file metadata
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FileMetadata {
|
pub struct FileMetadata {
|
||||||
/// Ruta absoluta del archivo
|
/// Absolute file path
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
/// Si el archivo existe físicamente
|
/// Whether the file physically exists
|
||||||
pub exists: bool,
|
pub exists: bool,
|
||||||
/// Tipo de entrada (archivo, directorio)
|
/// Entry type (file, directory)
|
||||||
pub entry_type: CacheEntryType,
|
pub entry_type: CacheEntryType,
|
||||||
/// Tamaño en bytes (para archivos)
|
/// Size in bytes (for files)
|
||||||
pub size: Option<u64>,
|
pub size: Option<u64>,
|
||||||
/// Tipo MIME (para archivos)
|
/// MIME type (for files)
|
||||||
pub mime_type: Option<String>,
|
pub mime_type: Option<String>,
|
||||||
/// Timestamp de creación (UNIX epoch seconds)
|
/// Creation timestamp (UNIX epoch seconds)
|
||||||
pub created_at: Option<u64>,
|
pub created_at: Option<u64>,
|
||||||
/// Timestamp de modificación (UNIX epoch seconds)
|
/// Modification timestamp (UNIX epoch seconds)
|
||||||
pub modified_at: Option<u64>,
|
pub modified_at: Option<u64>,
|
||||||
/// Acceso previo (usado para LRU)
|
/// Previous access (used for LRU)
|
||||||
pub last_access: Instant,
|
pub last_access: Instant,
|
||||||
/// Tiempo de expiración de la caché
|
/// Cache expiration time
|
||||||
pub expires_at: Instant,
|
pub expires_at: Instant,
|
||||||
/// Número de accesos a esta entrada
|
/// Number of accesses to this entry
|
||||||
pub access_count: usize,
|
pub access_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileMetadata {
|
impl FileMetadata {
|
||||||
/// Crea una nueva entrada de metadatos
|
/// Creates a new metadata entry
|
||||||
pub fn new(
|
pub fn new(
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
exists: bool,
|
exists: bool,
|
||||||
@@ -94,56 +94,56 @@ impl FileMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza el tiempo de último acceso
|
/// Updates the last access time
|
||||||
pub fn touch(&mut self) {
|
pub fn touch(&mut self) {
|
||||||
self.last_access = Instant::now();
|
self.last_access = Instant::now();
|
||||||
self.access_count += 1;
|
self.access_count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifica si la entrada ha expirado
|
/// Checks if the entry has expired
|
||||||
pub fn is_expired(&self) -> bool {
|
pub fn is_expired(&self) -> bool {
|
||||||
Instant::now() > self.expires_at
|
Instant::now() > self.expires_at
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza el tiempo de expiración con un nuevo TTL
|
/// Updates the expiration time with a new TTL
|
||||||
pub fn update_expiry(&mut self, ttl: Duration) {
|
pub fn update_expiry(&mut self, ttl: Duration) {
|
||||||
self.expires_at = Instant::now() + ttl;
|
self.expires_at = Instant::now() + ttl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Caché avanzada de metadatos de archivos
|
/// Advanced file metadata cache
|
||||||
pub struct FileMetadataCache {
|
pub struct FileMetadataCache {
|
||||||
/// Caché principal de metadatos
|
/// Main metadata cache
|
||||||
metadata_cache: RwLock<HashMap<PathBuf, FileMetadata>>,
|
metadata_cache: RwLock<HashMap<PathBuf, FileMetadata>>,
|
||||||
/// Cola LRU para administración de caché
|
/// LRU queue for cache management
|
||||||
lru_queue: RwLock<VecDeque<PathBuf>>,
|
lru_queue: RwLock<VecDeque<PathBuf>>,
|
||||||
/// Estadísticas de uso del caché
|
/// Cache usage statistics
|
||||||
stats: RwLock<CacheStats>,
|
stats: RwLock<CacheStats>,
|
||||||
/// Configuración global de la aplicación
|
/// Global application configuration
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
/// TTL adaptativo para entradas populares
|
/// Adaptive TTL for popular entries
|
||||||
ttl_multiplier: f64,
|
ttl_multiplier: f64,
|
||||||
/// Umbral de popularidad para TTL extendido
|
/// Popularity threshold for extended TTL
|
||||||
popularity_threshold: usize,
|
popularity_threshold: usize,
|
||||||
/// Tamaño máximo de caché
|
/// Maximum cache size
|
||||||
max_entries: usize,
|
max_entries: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileMetadataCache {
|
impl FileMetadataCache {
|
||||||
/// Crea una nueva instancia de caché de metadatos
|
/// Creates a new metadata cache instance
|
||||||
pub fn new(config: AppConfig, max_entries: usize) -> Self {
|
pub fn new(config: AppConfig, max_entries: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
metadata_cache: RwLock::new(HashMap::with_capacity(max_entries)),
|
metadata_cache: RwLock::new(HashMap::with_capacity(max_entries)),
|
||||||
lru_queue: RwLock::new(VecDeque::with_capacity(max_entries)),
|
lru_queue: RwLock::new(VecDeque::with_capacity(max_entries)),
|
||||||
stats: RwLock::new(CacheStats::default()),
|
stats: RwLock::new(CacheStats::default()),
|
||||||
config,
|
config,
|
||||||
ttl_multiplier: 5.0, // Entradas populares tienen 5x TTL
|
ttl_multiplier: 5.0, // Popular entries have 5x TTL
|
||||||
popularity_threshold: 10, // Después de 10 accesos se considera popular
|
popularity_threshold: 10, // After 10 accesses it's considered popular
|
||||||
max_entries,
|
max_entries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un objeto FileMetadata a partir de un objeto File
|
/// Creates a FileMetadata object from a File object
|
||||||
pub fn create_metadata_from_file(file: &File, abs_path: PathBuf) -> FileMetadata {
|
pub fn create_metadata_from_file(file: &File, abs_path: PathBuf) -> FileMetadata {
|
||||||
let entry_type = CacheEntryType::File;
|
let entry_type = CacheEntryType::File;
|
||||||
let size = Some(file.size());
|
let size = Some(file.size());
|
||||||
@@ -151,8 +151,8 @@ impl FileMetadataCache {
|
|||||||
let created_at = Some(file.created_at());
|
let created_at = Some(file.created_at());
|
||||||
let modified_at = Some(file.modified_at());
|
let modified_at = Some(file.modified_at());
|
||||||
|
|
||||||
// Usar un TTL estándar
|
// Use a standard TTL
|
||||||
let ttl = Duration::from_secs(60); // 1 minuto
|
let ttl = Duration::from_secs(60); // 1 minute
|
||||||
|
|
||||||
FileMetadata::new(
|
FileMetadata::new(
|
||||||
abs_path,
|
abs_path,
|
||||||
@@ -166,28 +166,28 @@ impl FileMetadataCache {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una instancia por defecto
|
/// Creates a default instance
|
||||||
pub fn default() -> Self {
|
pub fn default() -> Self {
|
||||||
Self::new(AppConfig::default(), 10_000)
|
Self::new(AppConfig::default(), 10_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una instancia de caché con configuración por defecto
|
/// Creates a cache instance with default configuration
|
||||||
pub fn default_with_config(config: AppConfig) -> Self {
|
pub fn default_with_config(config: AppConfig) -> Self {
|
||||||
Self::new(config, 50_000) // Caché más grande para sistema en producción
|
Self::new(config, 50_000) // Larger cache for production system
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene los metadatos de un archivo si están en caché
|
/// Gets file metadata if cached
|
||||||
pub async fn get_metadata(&self, path: &Path) -> Option<FileMetadata> {
|
pub async fn get_metadata(&self, path: &Path) -> Option<FileMetadata> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut cache = self.metadata_cache.write().await;
|
let mut cache = self.metadata_cache.write().await;
|
||||||
|
|
||||||
if let Some(metadata) = cache.get_mut(path) {
|
if let Some(metadata) = cache.get_mut(path) {
|
||||||
// Verificar si ha expirado
|
// Check if expired
|
||||||
if metadata.is_expired() {
|
if metadata.is_expired() {
|
||||||
// Eliminar de caché si expiró
|
// Remove from cache if expired
|
||||||
cache.remove(path);
|
cache.remove(path);
|
||||||
|
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
let mut stats = self.stats.write().await;
|
let mut stats = self.stats.write().await;
|
||||||
stats.misses += 1;
|
stats.misses += 1;
|
||||||
stats.expirations += 1;
|
stats.expirations += 1;
|
||||||
@@ -197,10 +197,10 @@ impl FileMetadataCache {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar tiempo de acceso
|
// Update access time
|
||||||
metadata.touch();
|
metadata.touch();
|
||||||
|
|
||||||
// Para entradas populares, extender TTL
|
// For popular entries, extend TTL
|
||||||
if metadata.access_count >= self.popularity_threshold {
|
if metadata.access_count >= self.popularity_threshold {
|
||||||
let new_ttl = match metadata.entry_type {
|
let new_ttl = match metadata.entry_type {
|
||||||
CacheEntryType::File => Duration::from_millis(
|
CacheEntryType::File => Duration::from_millis(
|
||||||
@@ -209,33 +209,33 @@ impl FileMetadataCache {
|
|||||||
CacheEntryType::Directory => Duration::from_millis(
|
CacheEntryType::Directory => Duration::from_millis(
|
||||||
(self.config.timeouts.dir_operation_ms as f64 * self.ttl_multiplier) as u64
|
(self.config.timeouts.dir_operation_ms as f64 * self.ttl_multiplier) as u64
|
||||||
),
|
),
|
||||||
_ => Duration::from_secs(60), // 1 minuto por defecto
|
_ => Duration::from_secs(60), // 1 minute by default
|
||||||
};
|
};
|
||||||
|
|
||||||
metadata.update_expiry(new_ttl);
|
metadata.update_expiry(new_ttl);
|
||||||
debug!("Extended TTL for popular entry: {}", path.display());
|
debug!("Extended TTL for popular entry: {}", path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calcular tiempo ahorrado aproximado
|
// Calculate approximate time saved
|
||||||
let elapsed = start_time.elapsed().as_millis() as u64;
|
let elapsed = start_time.elapsed().as_millis() as u64;
|
||||||
let estimated_io_time: u64 = 10; // Asumimos 10ms mínimo para operación de IO
|
let estimated_io_time: u64 = 10; // We assume 10ms minimum for IO operation
|
||||||
let time_saved = estimated_io_time.saturating_sub(elapsed);
|
let time_saved = estimated_io_time.saturating_sub(elapsed);
|
||||||
|
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
let mut stats = self.stats.write().await;
|
let mut stats = self.stats.write().await;
|
||||||
stats.hits += 1;
|
stats.hits += 1;
|
||||||
stats.time_saved_ms += time_saved;
|
stats.time_saved_ms += time_saved;
|
||||||
|
|
||||||
debug!("Cache hit for: {}", path.display());
|
debug!("Cache hit for: {}", path.display());
|
||||||
|
|
||||||
// Mantener también la cola LRU actualizada
|
// Also keep the LRU queue updated
|
||||||
self.update_lru(path.to_path_buf()).await;
|
self.update_lru(path.to_path_buf()).await;
|
||||||
|
|
||||||
// Clonar para retornar
|
// Clone to return
|
||||||
return Some(metadata.clone());
|
return Some(metadata.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// No encontrado en caché
|
// Not found in cache
|
||||||
let mut stats = self.stats.write().await;
|
let mut stats = self.stats.write().await;
|
||||||
stats.misses += 1;
|
stats.misses += 1;
|
||||||
|
|
||||||
@@ -243,20 +243,20 @@ impl FileMetadataCache {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza la cola LRU
|
/// Updates the LRU queue
|
||||||
async fn update_lru(&self, path: PathBuf) {
|
async fn update_lru(&self, path: PathBuf) {
|
||||||
let mut lru = self.lru_queue.write().await;
|
let mut lru = self.lru_queue.write().await;
|
||||||
|
|
||||||
// Eliminar si ya existe
|
// Remove if already exists
|
||||||
if let Some(pos) = lru.iter().position(|p| p == &path) {
|
if let Some(pos) = lru.iter().position(|p| p == &path) {
|
||||||
lru.remove(pos);
|
lru.remove(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agregar al final (más reciente)
|
// Add to the end (most recent)
|
||||||
lru.push_back(path);
|
lru.push_back(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifica si un archivo existe
|
/// Checks if a file exists
|
||||||
pub async fn exists(&self, path: &Path) -> Option<bool> {
|
pub async fn exists(&self, path: &Path) -> Option<bool> {
|
||||||
if let Some(metadata) = self.get_metadata(path).await {
|
if let Some(metadata) = self.get_metadata(path).await {
|
||||||
return Some(metadata.exists);
|
return Some(metadata.exists);
|
||||||
@@ -265,7 +265,7 @@ impl FileMetadataCache {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifica si un path es un directorio
|
/// Checks if a path is a directory
|
||||||
pub async fn is_dir(&self, path: &Path) -> Option<bool> {
|
pub async fn is_dir(&self, path: &Path) -> Option<bool> {
|
||||||
if let Some(metadata) = self.get_metadata(path).await {
|
if let Some(metadata) = self.get_metadata(path).await {
|
||||||
return Some(metadata.entry_type == CacheEntryType::Directory);
|
return Some(metadata.entry_type == CacheEntryType::Directory);
|
||||||
@@ -274,7 +274,7 @@ impl FileMetadataCache {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifica si un path es un archivo
|
/// Checks if a path is a file
|
||||||
pub async fn is_file(&self, path: &Path) -> Option<bool> {
|
pub async fn is_file(&self, path: &Path) -> Option<bool> {
|
||||||
if let Some(metadata) = self.get_metadata(path).await {
|
if let Some(metadata) = self.get_metadata(path).await {
|
||||||
return Some(metadata.entry_type == CacheEntryType::File);
|
return Some(metadata.entry_type == CacheEntryType::File);
|
||||||
@@ -283,7 +283,7 @@ impl FileMetadataCache {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene el tamaño de un archivo
|
/// Gets the size of a file
|
||||||
pub async fn get_size(&self, path: &Path) -> Option<u64> {
|
pub async fn get_size(&self, path: &Path) -> Option<u64> {
|
||||||
if let Some(metadata) = self.get_metadata(path).await {
|
if let Some(metadata) = self.get_metadata(path).await {
|
||||||
return metadata.size;
|
return metadata.size;
|
||||||
@@ -292,7 +292,7 @@ impl FileMetadataCache {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene el tipo MIME de un archivo
|
/// Gets the MIME type of a file
|
||||||
pub async fn get_mime_type(&self, path: &Path) -> Option<String> {
|
pub async fn get_mime_type(&self, path: &Path) -> Option<String> {
|
||||||
if let Some(metadata) = self.get_metadata(path).await {
|
if let Some(metadata) = self.get_metadata(path).await {
|
||||||
return metadata.mime_type;
|
return metadata.mime_type;
|
||||||
@@ -301,12 +301,12 @@ impl FileMetadataCache {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresca los metadatos de un path
|
/// Refreshes metadata for a path
|
||||||
pub async fn refresh_metadata(&self, path: &Path) -> Result<FileMetadata, std::io::Error> {
|
pub async fn refresh_metadata(&self, path: &Path) -> Result<FileMetadata, std::io::Error> {
|
||||||
// Realizar lectura real del sistema de archivos
|
// Perform actual filesystem read
|
||||||
let metadata = fs::metadata(path).await?;
|
let metadata = fs::metadata(path).await?;
|
||||||
|
|
||||||
// Determinar tipo de entrada
|
// Determine entry type
|
||||||
let entry_type = if metadata.is_dir() {
|
let entry_type = if metadata.is_dir() {
|
||||||
CacheEntryType::Directory
|
CacheEntryType::Directory
|
||||||
} else if metadata.is_file() {
|
} else if metadata.is_file() {
|
||||||
@@ -315,21 +315,21 @@ impl FileMetadataCache {
|
|||||||
CacheEntryType::Unknown
|
CacheEntryType::Unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
// Obtener tamaño para archivos
|
// Get size for files
|
||||||
let size = if metadata.is_file() {
|
let size = if metadata.is_file() {
|
||||||
Some(metadata.len())
|
Some(metadata.len())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Obtener tipo MIME para archivos
|
// Get MIME type for files
|
||||||
let mime_type = if metadata.is_file() {
|
let mime_type = if metadata.is_file() {
|
||||||
Some(from_path(path).first_or_octet_stream().to_string())
|
Some(from_path(path).first_or_octet_stream().to_string())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Obtener timestamps
|
// Get timestamps
|
||||||
let created_at = metadata.created()
|
let created_at = metadata.created()
|
||||||
.map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
|
.map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||||
.ok();
|
.ok();
|
||||||
@@ -338,14 +338,14 @@ impl FileMetadataCache {
|
|||||||
.map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
|
.map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
// Determinar TTL apropiado
|
// Determine appropriate TTL
|
||||||
let ttl = if metadata.is_dir() {
|
let ttl = if metadata.is_dir() {
|
||||||
Duration::from_millis(self.config.timeouts.dir_operation_ms)
|
Duration::from_millis(self.config.timeouts.dir_operation_ms)
|
||||||
} else {
|
} else {
|
||||||
Duration::from_millis(self.config.timeouts.file_operation_ms)
|
Duration::from_millis(self.config.timeouts.file_operation_ms)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Crear entrada de metadatos
|
// Create metadata entry
|
||||||
let file_metadata = FileMetadata::new(
|
let file_metadata = FileMetadata::new(
|
||||||
path.to_path_buf(),
|
path.to_path_buf(),
|
||||||
true,
|
true,
|
||||||
@@ -357,34 +357,34 @@ impl FileMetadataCache {
|
|||||||
ttl,
|
ttl,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Actualizar caché
|
// Update cache
|
||||||
self.update_cache(file_metadata.clone()).await;
|
self.update_cache(file_metadata.clone()).await;
|
||||||
|
|
||||||
Ok(file_metadata)
|
Ok(file_metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza la caché con nuevos metadatos
|
/// Updates the cache with new metadata
|
||||||
pub async fn update_cache(&self, metadata: FileMetadata) {
|
pub async fn update_cache(&self, metadata: FileMetadata) {
|
||||||
// Evitar caché llena antes de insertar
|
// Avoid full cache before inserting
|
||||||
self.ensure_capacity().await;
|
self.ensure_capacity().await;
|
||||||
|
|
||||||
let path = metadata.path.clone();
|
let path = metadata.path.clone();
|
||||||
|
|
||||||
// Insertar en caché
|
// Insert into cache
|
||||||
{
|
{
|
||||||
let mut cache = self.metadata_cache.write().await;
|
let mut cache = self.metadata_cache.write().await;
|
||||||
cache.insert(path.clone(), metadata);
|
cache.insert(path.clone(), metadata);
|
||||||
|
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
let mut stats = self.stats.write().await;
|
let mut stats = self.stats.write().await;
|
||||||
stats.inserts += 1;
|
stats.inserts += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar la cola LRU
|
// Update the LRU queue
|
||||||
self.update_lru(path).await;
|
self.update_lru(path).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asegura que hay espacio en la caché
|
/// Ensures there is space in the cache
|
||||||
async fn ensure_capacity(&self) {
|
async fn ensure_capacity(&self) {
|
||||||
let cache_size = {
|
let cache_size = {
|
||||||
let cache = self.metadata_cache.read().await;
|
let cache = self.metadata_cache.read().await;
|
||||||
@@ -392,15 +392,15 @@ impl FileMetadataCache {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if cache_size >= self.max_entries {
|
if cache_size >= self.max_entries {
|
||||||
self.evict_lru_entries(cache_size / 10).await; // Liberar 10%
|
self.evict_lru_entries(cache_size / 10).await; // Free up 10%
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina entradas menos recientemente usadas
|
/// Removes least recently used entries
|
||||||
async fn evict_lru_entries(&self, count: usize) {
|
async fn evict_lru_entries(&self, count: usize) {
|
||||||
let mut paths_to_remove = Vec::with_capacity(count);
|
let mut paths_to_remove = Vec::with_capacity(count);
|
||||||
|
|
||||||
// Obtener entries a eliminar de la cola LRU
|
// Get entries to remove from the LRU queue
|
||||||
{
|
{
|
||||||
let mut lru = self.lru_queue.write().await;
|
let mut lru = self.lru_queue.write().await;
|
||||||
for _ in 0..count {
|
for _ in 0..count {
|
||||||
@@ -412,7 +412,7 @@ impl FileMetadataCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eliminar de la caché principal
|
// Remove from the main cache
|
||||||
{
|
{
|
||||||
let mut cache = self.metadata_cache.write().await;
|
let mut cache = self.metadata_cache.write().await;
|
||||||
for path in paths_to_remove {
|
for path in paths_to_remove {
|
||||||
@@ -423,19 +423,19 @@ impl FileMetadataCache {
|
|||||||
debug!("Evicted {} LRU entries from cache", count);
|
debug!("Evicted {} LRU entries from cache", count);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invalidar una entrada específica de caché
|
/// Invalidate a specific cache entry
|
||||||
pub async fn invalidate(&self, path: &Path) {
|
pub async fn invalidate(&self, path: &Path) {
|
||||||
// Eliminar de la caché principal
|
// Remove from the main cache
|
||||||
{
|
{
|
||||||
let mut cache = self.metadata_cache.write().await;
|
let mut cache = self.metadata_cache.write().await;
|
||||||
cache.remove(path);
|
cache.remove(path);
|
||||||
|
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
let mut stats = self.stats.write().await;
|
let mut stats = self.stats.write().await;
|
||||||
stats.invalidations += 1;
|
stats.invalidations += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eliminar de la cola LRU
|
// Remove from the LRU queue
|
||||||
let path_buf = path.to_path_buf();
|
let path_buf = path.to_path_buf();
|
||||||
{
|
{
|
||||||
let mut lru = self.lru_queue.write().await;
|
let mut lru = self.lru_queue.write().await;
|
||||||
@@ -447,12 +447,12 @@ impl FileMetadataCache {
|
|||||||
debug!("Invalidated cache entry for: {}", path.display());
|
debug!("Invalidated cache entry for: {}", path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invalidar recursivamente entradas bajo un directorio
|
/// Recursively invalidate entries under a directory
|
||||||
pub async fn invalidate_directory(&self, dir_path: &Path) {
|
pub async fn invalidate_directory(&self, dir_path: &Path) {
|
||||||
let dir_str = dir_path.to_string_lossy().to_string();
|
let dir_str = dir_path.to_string_lossy().to_string();
|
||||||
let mut paths_to_remove = Vec::new();
|
let mut paths_to_remove = Vec::new();
|
||||||
|
|
||||||
// Encontrar todos los paths que comienzan con el directorio
|
// Find all paths that start with the directory
|
||||||
{
|
{
|
||||||
let cache = self.metadata_cache.read().await;
|
let cache = self.metadata_cache.read().await;
|
||||||
for path in cache.keys() {
|
for path in cache.keys() {
|
||||||
@@ -463,13 +463,13 @@ impl FileMetadataCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
{
|
{
|
||||||
let mut stats = self.stats.write().await;
|
let mut stats = self.stats.write().await;
|
||||||
stats.invalidations += paths_to_remove.len();
|
stats.invalidations += paths_to_remove.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eliminar cada path encontrado
|
// Remove each found path
|
||||||
for path in paths_to_remove {
|
for path in paths_to_remove {
|
||||||
self.invalidate(&path).await;
|
self.invalidate(&path).await;
|
||||||
}
|
}
|
||||||
@@ -477,18 +477,18 @@ impl FileMetadataCache {
|
|||||||
debug!("Invalidated directory and contents: {}", dir_path.display());
|
debug!("Invalidated directory and contents: {}", dir_path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtener estadísticas actuales de la caché
|
/// Get current cache statistics
|
||||||
pub async fn get_stats(&self) -> CacheStats {
|
pub async fn get_stats(&self) -> CacheStats {
|
||||||
let stats = self.stats.read().await;
|
let stats = self.stats.read().await;
|
||||||
stats.clone()
|
stats.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Limpia todas las entradas expiradas de la caché
|
/// Clears all expired entries from the cache
|
||||||
pub async fn clear_expired(&self) {
|
pub async fn clear_expired(&self) {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let mut paths_to_remove = Vec::new();
|
let mut paths_to_remove = Vec::new();
|
||||||
|
|
||||||
// Encontrar entradas expiradas
|
// Find expired entries
|
||||||
{
|
{
|
||||||
let cache = self.metadata_cache.read().await;
|
let cache = self.metadata_cache.read().await;
|
||||||
for (path, metadata) in cache.iter() {
|
for (path, metadata) in cache.iter() {
|
||||||
@@ -498,16 +498,16 @@ impl FileMetadataCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
{
|
{
|
||||||
let mut stats = self.stats.write().await;
|
let mut stats = self.stats.write().await;
|
||||||
stats.expirations += paths_to_remove.len();
|
stats.expirations += paths_to_remove.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guardar la cantidad de entradas para el logging
|
// Save the number of entries for logging
|
||||||
let num_paths = paths_to_remove.len();
|
let num_paths = paths_to_remove.len();
|
||||||
|
|
||||||
// Eliminar entradas expiradas
|
// Remove expired entries
|
||||||
for path in paths_to_remove {
|
for path in paths_to_remove {
|
||||||
self.invalidate(&path).await;
|
self.invalidate(&path).await;
|
||||||
}
|
}
|
||||||
@@ -515,19 +515,19 @@ impl FileMetadataCache {
|
|||||||
debug!("Cleared {} expired entries from cache", num_paths);
|
debug!("Cleared {} expired entries from cache", num_paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inicia el proceso de limpieza periódica
|
/// Starts the periodic cleanup process
|
||||||
pub fn start_cleanup_task(cache: Arc<Self>) -> BoxFuture<'static, ()> {
|
pub fn start_cleanup_task(cache: Arc<Self>) -> BoxFuture<'static, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let cleanup_interval = Duration::from_secs(60); // Cada minuto
|
let cleanup_interval = Duration::from_secs(60); // Every minute
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Esperar el intervalo
|
// Wait for the interval
|
||||||
time::sleep(cleanup_interval).await;
|
time::sleep(cleanup_interval).await;
|
||||||
|
|
||||||
// Limpiar entradas expiradas
|
// Clean expired entries
|
||||||
cache.clear_expired().await;
|
cache.clear_expired().await;
|
||||||
|
|
||||||
// Registrar estadísticas
|
// Log statistics
|
||||||
let stats = cache.get_stats().await;
|
let stats = cache.get_stats().await;
|
||||||
let cache_size = {
|
let cache_size = {
|
||||||
let cache_map = cache.metadata_cache.read().await;
|
let cache_map = cache.metadata_cache.read().await;
|
||||||
@@ -550,12 +550,12 @@ impl FileMetadataCache {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Precarga metadatos de directorios completos (útil para inicialización)
|
/// Preloads metadata for entire directories (useful for initialization)
|
||||||
pub async fn preload_directory(&self, dir_path: &Path, recursive: bool, max_depth: usize) -> Result<usize, std::io::Error> {
|
pub async fn preload_directory(&self, dir_path: &Path, recursive: bool, max_depth: usize) -> Result<usize, std::io::Error> {
|
||||||
self._preload_directory_internal(dir_path, recursive, max_depth, 0).await
|
self._preload_directory_internal(dir_path, recursive, max_depth, 0).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementación interna de precarga con seguimiento de profundidad
|
/// Internal preload implementation with depth tracking
|
||||||
async fn _preload_directory_internal(
|
async fn _preload_directory_internal(
|
||||||
&self,
|
&self,
|
||||||
dir_path: &Path,
|
dir_path: &Path,
|
||||||
@@ -568,20 +568,20 @@ impl FileMetadataCache {
|
|||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtener entradas del directorio
|
// Get directory entries
|
||||||
let mut entries = fs::read_dir(dir_path).await?;
|
let mut entries = fs::read_dir(dir_path).await?;
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Procesar cada entrada
|
// Process each entry
|
||||||
while let Some(entry) = entries.next_entry().await? {
|
while let Some(entry) = entries.next_entry().await? {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
let metadata = fs::metadata(&path).await?;
|
let metadata = fs::metadata(&path).await?;
|
||||||
|
|
||||||
// Refrescar metadatos de esta entrada
|
// Refresh metadata for this entry
|
||||||
self.refresh_metadata(&path).await?;
|
self.refresh_metadata(&path).await?;
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
// Recursivamente procesar subdirectorios si es necesario
|
// Recursively process subdirectories if needed
|
||||||
if recursive && metadata.is_dir() {
|
if recursive && metadata.is_dir() {
|
||||||
// Box to break recursion
|
// Box to break recursion
|
||||||
count += self._preload_directory_internal(
|
count += self._preload_directory_internal(
|
||||||
@@ -657,37 +657,37 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_cache_operations() {
|
async fn test_cache_operations() {
|
||||||
// Crear directorio temporal para pruebas
|
// Create temporary directory for tests
|
||||||
let temp_dir = tempdir().unwrap();
|
let temp_dir = tempdir().unwrap();
|
||||||
let file_path = temp_dir.path().join("test_file.txt");
|
let file_path = temp_dir.path().join("test_file.txt");
|
||||||
|
|
||||||
// Crear un archivo de prueba
|
// Create a test file
|
||||||
let mut file = File::create(&file_path).await.unwrap();
|
let mut file = File::create(&file_path).await.unwrap();
|
||||||
file.write_all(b"test content").await.unwrap();
|
file.write_all(b"test content").await.unwrap();
|
||||||
file.flush().await.unwrap();
|
file.flush().await.unwrap();
|
||||||
drop(file);
|
drop(file);
|
||||||
|
|
||||||
// Crear caché
|
// Create cache
|
||||||
let config = AppConfig::default();
|
let config = AppConfig::default();
|
||||||
let cache = FileMetadataCache::new(config, 1000);
|
let cache = FileMetadataCache::new(config, 1000);
|
||||||
|
|
||||||
// Verificar miss inicial
|
// Verify initial miss
|
||||||
assert!(cache.exists(&file_path).await.is_none());
|
assert!(cache.exists(&file_path).await.is_none());
|
||||||
|
|
||||||
// Refrescar y verificar hit
|
// Refresh and verify hit
|
||||||
let metadata = cache.refresh_metadata(&file_path).await.unwrap();
|
let metadata = cache.refresh_metadata(&file_path).await.unwrap();
|
||||||
assert_eq!(metadata.entry_type, CacheEntryType::File);
|
assert_eq!(metadata.entry_type, CacheEntryType::File);
|
||||||
assert_eq!(metadata.size, Some(12)); // "test content" = 12 bytes
|
assert_eq!(metadata.size, Some(12)); // "test content" = 12 bytes
|
||||||
|
|
||||||
// Verificar que ahora existe en caché
|
// Verify it now exists in cache
|
||||||
assert_eq!(cache.exists(&file_path).await, Some(true));
|
assert_eq!(cache.exists(&file_path).await, Some(true));
|
||||||
assert_eq!(cache.is_file(&file_path).await, Some(true));
|
assert_eq!(cache.is_file(&file_path).await, Some(true));
|
||||||
|
|
||||||
// Invalidar y verificar que ya no existe en caché
|
// Invalidate and verify it no longer exists in cache
|
||||||
cache.invalidate(&file_path).await;
|
cache.invalidate(&file_path).await;
|
||||||
assert!(cache.exists(&file_path).await.is_none());
|
assert!(cache.exists(&file_path).await.is_none());
|
||||||
|
|
||||||
// Verificar estadísticas
|
// Verify statistics
|
||||||
let stats = cache.get_stats().await;
|
let stats = cache.get_stats().await;
|
||||||
assert_eq!(stats.inserts, 1);
|
assert_eq!(stats.inserts, 1);
|
||||||
assert_eq!(stats.invalidations, 1);
|
assert_eq!(stats.invalidations, 1);
|
||||||
@@ -696,7 +696,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_directory_operations() {
|
async fn test_directory_operations() {
|
||||||
// Crear estructura de directorios para pruebas
|
// Create directory structure for tests
|
||||||
let temp_dir = tempdir().unwrap();
|
let temp_dir = tempdir().unwrap();
|
||||||
// Canonicalize to handle macOS /var -> /private/var symlinks
|
// Canonicalize to handle macOS /var -> /private/var symlinks
|
||||||
let base_path = temp_dir.path().canonicalize().unwrap();
|
let base_path = temp_dir.path().canonicalize().unwrap();
|
||||||
@@ -709,24 +709,24 @@ mod tests {
|
|||||||
File::create(&file1).await.unwrap();
|
File::create(&file1).await.unwrap();
|
||||||
File::create(&file2).await.unwrap();
|
File::create(&file2).await.unwrap();
|
||||||
|
|
||||||
// Crear caché
|
// Create cache
|
||||||
let config = AppConfig::default();
|
let config = AppConfig::default();
|
||||||
let cache = FileMetadataCache::new(config, 1000);
|
let cache = FileMetadataCache::new(config, 1000);
|
||||||
|
|
||||||
// Precargar directorio recursivamente
|
// Preload directory recursively
|
||||||
// preload_directory caches the *contents* of the directory, not the root itself
|
// preload_directory caches the *contents* of the directory, not the root itself
|
||||||
let count = cache.preload_directory(&base_path, true, 2).await.unwrap();
|
let count = cache.preload_directory(&base_path, true, 2).await.unwrap();
|
||||||
assert_eq!(count, 3); // subdir, file1, file2
|
assert_eq!(count, 3); // subdir, file1, file2
|
||||||
|
|
||||||
// Verificar existencia en caché (solo contenido, no la raíz)
|
// Verify existence in cache (only contents, not the root)
|
||||||
assert_eq!(cache.is_dir(&sub_dir).await, Some(true));
|
assert_eq!(cache.is_dir(&sub_dir).await, Some(true));
|
||||||
assert_eq!(cache.is_file(&file1).await, Some(true));
|
assert_eq!(cache.is_file(&file1).await, Some(true));
|
||||||
assert_eq!(cache.is_file(&file2).await, Some(true));
|
assert_eq!(cache.is_file(&file2).await, Some(true));
|
||||||
|
|
||||||
// Invalidar directorio y contenido
|
// Invalidate directory and contents
|
||||||
cache.invalidate_directory(&base_path).await;
|
cache.invalidate_directory(&base_path).await;
|
||||||
|
|
||||||
// Verificar que nada existe en caché
|
// Verify nothing exists in cache
|
||||||
assert!(cache.exists(&sub_dir).await.is_none());
|
assert!(cache.exists(&sub_dir).await.is_none());
|
||||||
assert!(cache.exists(&file1).await.is_none());
|
assert!(cache.exists(&file1).await.is_none());
|
||||||
assert!(cache.exists(&file2).await.is_none());
|
assert!(cache.exists(&file2).await.is_none());
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ impl IdMappingOptimizer {
|
|||||||
{
|
{
|
||||||
let cache = self.path_to_id_cache.read().await;
|
let cache = self.path_to_id_cache.read().await;
|
||||||
if let Some((id, _)) = cache.get(&path_str) {
|
if let Some((id, _)) = cache.get(&path_str) {
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
{
|
{
|
||||||
let mut stats = self.stats.write().await;
|
let mut stats = self.stats.write().await;
|
||||||
stats.get_id_hits += 1;
|
stats.get_id_hits += 1;
|
||||||
@@ -189,19 +189,19 @@ impl IdMappingOptimizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si no está en caché, agregar a la cola de batch
|
// If not in cache, add to batch queue
|
||||||
{
|
{
|
||||||
let mut batch_queue = self.pending_batch.lock().await;
|
let mut batch_queue = self.pending_batch.lock().await;
|
||||||
batch_queue.path_to_id_requests.insert(path_str);
|
batch_queue.path_to_id_requests.insert(path_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No encontrado en caché, debe procesarse en batch
|
// Not found in cache, must be processed in batch
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Procesa las solicitudes pendientes en batch
|
/// Processes pending requests in batch
|
||||||
async fn process_batch(&self) -> Result<BatchResult, IdMappingError> {
|
async fn process_batch(&self) -> Result<BatchResult, IdMappingError> {
|
||||||
// Adquirir permiso para operación batch
|
// Acquire permit for batch operation
|
||||||
let _permit = self.batch_limiter.acquire().await.unwrap();
|
let _permit = self.batch_limiter.acquire().await.unwrap();
|
||||||
|
|
||||||
// Get pending requests
|
// Get pending requests
|
||||||
@@ -214,13 +214,13 @@ impl IdMappingOptimizer {
|
|||||||
(paths, ids)
|
(paths, ids)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Crear resultados
|
// Create results
|
||||||
let mut result = BatchResult {
|
let mut result = BatchResult {
|
||||||
path_to_id: HashMap::with_capacity(path_requests.len()),
|
path_to_id: HashMap::with_capacity(path_requests.len()),
|
||||||
id_to_path: HashMap::with_capacity(id_requests.len()),
|
id_to_path: HashMap::with_capacity(id_requests.len()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Procesar solicitudes path->id en batch
|
// Process path->id requests in batch
|
||||||
for path_str in path_requests {
|
for path_str in path_requests {
|
||||||
let path = StoragePath::from_string(&path_str);
|
let path = StoragePath::from_string(&path_str);
|
||||||
match self.base_service.get_or_create_id(&path).await {
|
match self.base_service.get_or_create_id(&path).await {
|
||||||
@@ -230,12 +230,12 @@ impl IdMappingOptimizer {
|
|||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error batch-processing path {}: {}", path_str, e);
|
error!("Error batch-processing path {}: {}", path_str, e);
|
||||||
// Continuar con las demás solicitudes
|
// Continue with remaining requests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Procesar solicitudes id->path en batch
|
// Process id->path requests in batch
|
||||||
for id in id_requests {
|
for id in id_requests {
|
||||||
match self.base_service.get_path_by_id(&id).await {
|
match self.base_service.get_path_by_id(&id).await {
|
||||||
Ok(path) => {
|
Ok(path) => {
|
||||||
@@ -245,7 +245,7 @@ impl IdMappingOptimizer {
|
|||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error batch-processing ID {}: {}", id, e);
|
error!("Error batch-processing ID {}: {}", id, e);
|
||||||
// Continuar con las demás solicitudes
|
// Continue with remaining requests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,14 +266,14 @@ impl IdMappingOptimizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
{
|
{
|
||||||
let mut stats = self.stats.write().await;
|
let mut stats = self.stats.write().await;
|
||||||
stats.batch_operations += 1;
|
stats.batch_operations += 1;
|
||||||
stats.batch_items_processed += result.path_to_id.len() + result.id_to_path.len();
|
stats.batch_items_processed += result.path_to_id.len() + result.id_to_path.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guardar los cambios al disco en segundo plano
|
// Save changes to disk in the background
|
||||||
let service_clone = self.base_service.clone();
|
let service_clone = self.base_service.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = service_clone.save_pending_changes().await {
|
if let Err(e) = service_clone.save_pending_changes().await {
|
||||||
@@ -284,15 +284,15 @@ impl IdMappingOptimizer {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fuerza el procesamiento de solicitudes pendientes si hay suficientes
|
/// Forces processing of pending requests if there are enough
|
||||||
async fn trigger_batch_if_needed(&self, min_batch_size: usize) -> Result<(), IdMappingError> {
|
async fn trigger_batch_if_needed(&self, min_batch_size: usize) -> Result<(), IdMappingError> {
|
||||||
// Verificar si hay suficientes solicitudes pendientes
|
// Check if there are enough pending requests
|
||||||
let should_process = {
|
let should_process = {
|
||||||
let batch_queue = self.pending_batch.lock().await;
|
let batch_queue = self.pending_batch.lock().await;
|
||||||
batch_queue.path_to_id_requests.len() + batch_queue.id_to_path_requests.len() >= min_batch_size
|
batch_queue.path_to_id_requests.len() + batch_queue.id_to_path_requests.len() >= min_batch_size
|
||||||
};
|
};
|
||||||
|
|
||||||
// Procesar si es necesario
|
// Process if necessary
|
||||||
if should_process {
|
if should_process {
|
||||||
self.process_batch().await?;
|
self.process_batch().await?;
|
||||||
}
|
}
|
||||||
@@ -300,17 +300,17 @@ impl IdMappingOptimizer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Precargar un conjunto de rutas para obtener sus IDs en batch
|
/// Preload a set of paths to get their IDs in batch
|
||||||
pub async fn preload_paths(&self, paths: Vec<StoragePath>) -> Result<(), IdMappingError> {
|
pub async fn preload_paths(&self, paths: Vec<StoragePath>) -> Result<(), IdMappingError> {
|
||||||
// Solo proceder si hay rutas para cargar
|
// Only proceed if there are paths to load
|
||||||
if paths.is_empty() {
|
if paths.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rutas que debemos cargar (las que no están en caché)
|
// Paths we need to load (those not in cache)
|
||||||
let mut paths_to_load = Vec::new();
|
let mut paths_to_load = Vec::new();
|
||||||
|
|
||||||
// Verificar primero el caché
|
// Check cache first
|
||||||
{
|
{
|
||||||
let cache = self.path_to_id_cache.read().await;
|
let cache = self.path_to_id_cache.read().await;
|
||||||
for path in paths {
|
for path in paths {
|
||||||
@@ -321,12 +321,12 @@ impl IdMappingOptimizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si todos estaban en caché, terminar
|
// If all were in cache, finish
|
||||||
if paths_to_load.is_empty() {
|
if paths_to_load.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agregar rutas a la cola para procesamiento batch
|
// Add paths to queue for batch processing
|
||||||
{
|
{
|
||||||
let mut batch_queue = self.pending_batch.lock().await;
|
let mut batch_queue = self.pending_batch.lock().await;
|
||||||
for path in paths_to_load {
|
for path in paths_to_load {
|
||||||
@@ -334,23 +334,23 @@ impl IdMappingOptimizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecutar procesamiento batch inmediatamente
|
// Execute batch processing immediately
|
||||||
self.process_batch().await?;
|
self.process_batch().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Precargar un conjunto de IDs para obtener sus rutas en batch
|
/// Preload a set of IDs to get their paths in batch
|
||||||
pub async fn preload_ids(&self, ids: Vec<String>) -> Result<(), IdMappingError> {
|
pub async fn preload_ids(&self, ids: Vec<String>) -> Result<(), IdMappingError> {
|
||||||
// Solo proceder si hay IDs para cargar
|
// Only proceed if there are IDs to load
|
||||||
if ids.is_empty() {
|
if ids.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDs que debemos cargar (los que no están en caché)
|
// IDs we need to load (those not in cache)
|
||||||
let mut ids_to_load = Vec::new();
|
let mut ids_to_load = Vec::new();
|
||||||
|
|
||||||
// Verificar primero el caché
|
// Check cache first
|
||||||
{
|
{
|
||||||
let cache = self.id_to_path_cache.read().await;
|
let cache = self.id_to_path_cache.read().await;
|
||||||
for id in ids {
|
for id in ids {
|
||||||
@@ -360,12 +360,12 @@ impl IdMappingOptimizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si todos estaban en caché, terminar
|
// If all were in cache, finish
|
||||||
if ids_to_load.is_empty() {
|
if ids_to_load.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agregar IDs a la cola para procesamiento batch
|
// Add IDs to queue for batch processing
|
||||||
{
|
{
|
||||||
let mut batch_queue = self.pending_batch.lock().await;
|
let mut batch_queue = self.pending_batch.lock().await;
|
||||||
for id in ids_to_load {
|
for id in ids_to_load {
|
||||||
@@ -373,7 +373,7 @@ impl IdMappingOptimizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecutar procesamiento batch inmediatamente
|
// Execute batch processing immediately
|
||||||
self.process_batch().await?;
|
self.process_batch().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -383,7 +383,7 @@ impl IdMappingOptimizer {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl IdMappingPort for IdMappingOptimizer {
|
impl IdMappingPort for IdMappingOptimizer {
|
||||||
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> {
|
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> {
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
{
|
{
|
||||||
let mut stats = self.stats.write().await;
|
let mut stats = self.stats.write().await;
|
||||||
stats.get_id_queries += 1;
|
stats.get_id_queries += 1;
|
||||||
@@ -391,7 +391,7 @@ impl IdMappingPort for IdMappingOptimizer {
|
|||||||
|
|
||||||
let path_str = path.to_string();
|
let path_str = path.to_string();
|
||||||
|
|
||||||
// Verificar primero en el caché
|
// Check cache first
|
||||||
{
|
{
|
||||||
let cache = self.path_to_id_cache.read().await;
|
let cache = self.path_to_id_cache.read().await;
|
||||||
if let Some((id, _)) = cache.get(&path_str) {
|
if let Some((id, _)) = cache.get(&path_str) {
|
||||||
@@ -443,7 +443,7 @@ impl IdMappingPort for IdMappingOptimizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError> {
|
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError> {
|
||||||
// Actualizar estadísticas
|
// Update statistics
|
||||||
{
|
{
|
||||||
let mut stats = self.stats.write().await;
|
let mut stats = self.stats.write().await;
|
||||||
stats.path_by_id_queries += 1;
|
stats.path_by_id_queries += 1;
|
||||||
@@ -493,21 +493,21 @@ impl IdMappingPort for IdMappingOptimizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> {
|
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> {
|
||||||
// Invalidar caché para este ID
|
// Invalidate cache for this ID
|
||||||
{
|
{
|
||||||
let mut id_cache = self.id_to_path_cache.write().await;
|
let mut id_cache = self.id_to_path_cache.write().await;
|
||||||
let mut path_cache = self.path_to_id_cache.write().await;
|
let mut path_cache = self.path_to_id_cache.write().await;
|
||||||
|
|
||||||
// Eliminar la entrada del ID
|
// Remove the ID entry
|
||||||
if let Some((old_path, _)) = id_cache.remove(id) {
|
if let Some((old_path, _)) = id_cache.remove(id) {
|
||||||
path_cache.remove(&old_path);
|
path_cache.remove(&old_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar en el servicio base
|
// Update in the base service
|
||||||
let result = self.base_service.update_path(id, new_path).await?;
|
let result = self.base_service.update_path(id, new_path).await?;
|
||||||
|
|
||||||
// Actualizar caché con el nuevo mapeo
|
// Update cache with new mapping
|
||||||
{
|
{
|
||||||
let mut id_cache = self.id_to_path_cache.write().await;
|
let mut id_cache = self.id_to_path_cache.write().await;
|
||||||
let mut path_cache = self.path_to_id_cache.write().await;
|
let mut path_cache = self.path_to_id_cache.write().await;
|
||||||
@@ -523,25 +523,25 @@ impl IdMappingPort for IdMappingOptimizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_id(&self, id: &str) -> Result<(), DomainError> {
|
async fn remove_id(&self, id: &str) -> Result<(), DomainError> {
|
||||||
// Invalidar caché para este ID
|
// Invalidate cache for this ID
|
||||||
{
|
{
|
||||||
let mut id_cache = self.id_to_path_cache.write().await;
|
let mut id_cache = self.id_to_path_cache.write().await;
|
||||||
let mut path_cache = self.path_to_id_cache.write().await;
|
let mut path_cache = self.path_to_id_cache.write().await;
|
||||||
|
|
||||||
// Eliminar la entrada del ID
|
// Remove the ID entry
|
||||||
if let Some((path, _)) = id_cache.remove(id) {
|
if let Some((path, _)) = id_cache.remove(id) {
|
||||||
path_cache.remove(&path);
|
path_cache.remove(&path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eliminar en el servicio base
|
// Remove from the base service
|
||||||
self.base_service.remove_id(id).await?;
|
self.base_service.remove_id(id).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_changes(&self) -> Result<(), DomainError> {
|
async fn save_changes(&self) -> Result<(), DomainError> {
|
||||||
// Delegar al servicio base
|
// Delegate to the base service
|
||||||
self.base_service.save_changes().await?;
|
self.base_service.save_changes().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -569,15 +569,15 @@ mod tests {
|
|||||||
|
|
||||||
let path = StoragePath::from_string("/test/file.txt");
|
let path = StoragePath::from_string("/test/file.txt");
|
||||||
|
|
||||||
// Primera llamada debería usar el servicio base
|
// First call should use the base service
|
||||||
let id = optimizer.get_or_create_id(&path).await.unwrap();
|
let id = optimizer.get_or_create_id(&path).await.unwrap();
|
||||||
assert!(!id.is_empty(), "ID should not be empty");
|
assert!(!id.is_empty(), "ID should not be empty");
|
||||||
|
|
||||||
// Segunda llamada debería usar caché
|
// Second call should use cache
|
||||||
let id2 = optimizer.get_or_create_id(&path).await.unwrap();
|
let id2 = optimizer.get_or_create_id(&path).await.unwrap();
|
||||||
assert_eq!(id, id2, "Same path should return same ID");
|
assert_eq!(id, id2, "Same path should return same ID");
|
||||||
|
|
||||||
// Verificar estadísticas de caché
|
// Verify cache statistics
|
||||||
let stats = optimizer.get_stats().await;
|
let stats = optimizer.get_stats().await;
|
||||||
assert_eq!(stats.get_id_queries, 2, "Should have 2 queries");
|
assert_eq!(stats.get_id_queries, 2, "Should have 2 queries");
|
||||||
assert_eq!(stats.get_id_hits, 1, "Should have 1 hit");
|
assert_eq!(stats.get_id_hits, 1, "Should have 1 hit");
|
||||||
@@ -587,27 +587,27 @@ mod tests {
|
|||||||
async fn test_batch_processing() {
|
async fn test_batch_processing() {
|
||||||
let (_, optimizer) = create_test_service().await;
|
let (_, optimizer) = create_test_service().await;
|
||||||
|
|
||||||
// Crear un lote de rutas
|
// Create a batch of paths
|
||||||
let mut paths = Vec::new();
|
let mut paths = Vec::new();
|
||||||
for i in 0..50 {
|
for i in 0..50 {
|
||||||
paths.push(StoragePath::from_string(&format!("/test/batch/file{}.txt", i)));
|
paths.push(StoragePath::from_string(&format!("/test/batch/file{}.txt", i)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precargar las rutas
|
// Preload the paths
|
||||||
optimizer.preload_paths(paths.clone()).await.unwrap();
|
optimizer.preload_paths(paths.clone()).await.unwrap();
|
||||||
|
|
||||||
// Verificar que todas están en caché
|
// Verify all are in cache
|
||||||
for path in &paths {
|
for path in &paths {
|
||||||
let id = optimizer.get_or_create_id(path).await.unwrap();
|
let id = optimizer.get_or_create_id(path).await.unwrap();
|
||||||
assert!(!id.is_empty(), "ID should be available for path");
|
assert!(!id.is_empty(), "ID should be available for path");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar estadísticas
|
// Verify statistics
|
||||||
let stats = optimizer.get_stats().await;
|
let stats = optimizer.get_stats().await;
|
||||||
assert_eq!(stats.batch_operations, 1, "Should have 1 batch operation");
|
assert_eq!(stats.batch_operations, 1, "Should have 1 batch operation");
|
||||||
assert!(stats.batch_items_processed >= 50, "Should have processed at least 50 items");
|
assert!(stats.batch_items_processed >= 50, "Should have processed at least 50 items");
|
||||||
|
|
||||||
// Verificar que todas las consultas posteriores son hits en caché
|
// Verify all subsequent queries are cache hits
|
||||||
assert_eq!(stats.get_id_hits, 50, "All subsequente queries should be cache hits");
|
assert_eq!(stats.get_id_hits, 50, "All subsequente queries should be cache hits");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,21 +615,21 @@ mod tests {
|
|||||||
async fn test_cache_cleanup() {
|
async fn test_cache_cleanup() {
|
||||||
let (_, optimizer) = create_test_service().await;
|
let (_, optimizer) = create_test_service().await;
|
||||||
|
|
||||||
// Crear algunas entradas
|
// Create some entries
|
||||||
let path = StoragePath::from_string("/test/cleanup.txt");
|
let path = StoragePath::from_string("/test/cleanup.txt");
|
||||||
let id = optimizer.get_or_create_id(&path).await.unwrap();
|
let id = optimizer.get_or_create_id(&path).await.unwrap();
|
||||||
|
|
||||||
// Verificar estadísticas iniciales
|
// Verify initial statistics
|
||||||
{
|
{
|
||||||
let stats = optimizer.get_stats().await;
|
let stats = optimizer.get_stats().await;
|
||||||
assert_eq!(stats.get_id_queries, 1, "Should have 1 query");
|
assert_eq!(stats.get_id_queries, 1, "Should have 1 query");
|
||||||
assert_eq!(stats.get_id_hits, 0, "Should have 0 hits");
|
assert_eq!(stats.get_id_hits, 0, "Should have 0 hits");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecutar limpieza (no debería eliminar nada todavía)
|
// Run cleanup (should not remove anything yet)
|
||||||
optimizer.cleanup_cache().await;
|
optimizer.cleanup_cache().await;
|
||||||
|
|
||||||
// Verificar que el caché sigue funcionando
|
// Verify cache is still working
|
||||||
let id2 = optimizer.get_or_create_id(&path).await.unwrap();
|
let id2 = optimizer.get_or_create_id(&path).await.unwrap();
|
||||||
assert_eq!(id, id2, "Cache should still work after cleanup");
|
assert_eq!(id, id2, "Cache should still work after cleanup");
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::common::errors::{DomainError, ErrorKind};
|
|||||||
use crate::application::ports::outbound::IdMappingPort;
|
use crate::application::ports::outbound::IdMappingPort;
|
||||||
use crate::common::config::TimeoutConfig;
|
use crate::common::config::TimeoutConfig;
|
||||||
|
|
||||||
/// Error específico para el servicio de mapeo de IDs
|
/// Specific error for the ID mapping service
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum IdMappingError {
|
pub enum IdMappingError {
|
||||||
#[error("ID not found: {0}")]
|
#[error("ID not found: {0}")]
|
||||||
@@ -31,7 +31,7 @@ pub enum IdMappingError {
|
|||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementar conversión de IdMappingError a DomainError
|
// Implement conversion from IdMappingError to DomainError
|
||||||
impl From<IdMappingError> for DomainError {
|
impl From<IdMappingError> for DomainError {
|
||||||
fn from(err: IdMappingError) -> Self {
|
fn from(err: IdMappingError) -> Self {
|
||||||
match err {
|
match err {
|
||||||
@@ -59,25 +59,25 @@ impl From<IdMappingError> for DomainError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Estructura para almacenar IDs mapeados a sus rutas
|
/// Structure to store IDs mapped to their paths
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
struct IdMap {
|
struct IdMap {
|
||||||
path_to_id: HashMap<String, String>,
|
path_to_id: HashMap<String, String>,
|
||||||
id_to_path: HashMap<String, String>, // Campo para búsqueda bidireccional eficiente
|
id_to_path: HashMap<String, String>, // Field for efficient bidirectional lookup
|
||||||
version: u32, // Versión para detectar cambios
|
version: u32, // Version to detect changes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Servicio para gestionar mapeos entre rutas y IDs únicos
|
/// Service to manage mappings between paths and unique IDs
|
||||||
pub struct IdMappingService {
|
pub struct IdMappingService {
|
||||||
map_path: PathBuf,
|
map_path: PathBuf,
|
||||||
id_map: RwLock<IdMap>,
|
id_map: RwLock<IdMap>,
|
||||||
save_mutex: Mutex<()>, // Para evitar múltiples guardados concurrentes
|
save_mutex: Mutex<()>, // To prevent multiple concurrent saves
|
||||||
timeouts: TimeoutConfig,
|
timeouts: TimeoutConfig,
|
||||||
pending_save: RwLock<bool>, // Indica si hay cambios pendientes
|
pending_save: RwLock<bool>, // Indicates if there are pending changes
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IdMappingService {
|
impl IdMappingService {
|
||||||
/// Crea un nuevo servicio de mapeo de IDs
|
/// Creates a new ID mapping service
|
||||||
pub async fn new(map_path: PathBuf) -> Result<Self, DomainError> {
|
pub async fn new(map_path: PathBuf) -> Result<Self, DomainError> {
|
||||||
let timeouts = TimeoutConfig::default();
|
let timeouts = TimeoutConfig::default();
|
||||||
let id_map = Self::load_id_map(&map_path, &timeouts).await?;
|
let id_map = Self::load_id_map(&map_path, &timeouts).await?;
|
||||||
@@ -91,7 +91,7 @@ impl IdMappingService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un servicio de mapeo de IDs en memoria (para pruebas)
|
/// Creates an in-memory ID mapping service (for testing)
|
||||||
///
|
///
|
||||||
/// Similar functionality as new_in_memory but with a simpler signature for dummy use
|
/// Similar functionality as new_in_memory but with a simpler signature for dummy use
|
||||||
pub fn dummy() -> Self {
|
pub fn dummy() -> Self {
|
||||||
@@ -104,7 +104,7 @@ impl IdMappingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un servicio de mapeo de IDs en memoria (para pruebas - versión original)
|
/// Creates an in-memory ID mapping service (for testing - original version)
|
||||||
pub fn new_in_memory() -> Self {
|
pub fn new_in_memory() -> Self {
|
||||||
Self {
|
Self {
|
||||||
map_path: PathBuf::from("memory"),
|
map_path: PathBuf::from("memory"),
|
||||||
@@ -115,10 +115,10 @@ impl IdMappingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Carga el mapa de IDs desde disco con manejo robusto de errores
|
/// Loads the ID map from disk with robust error handling
|
||||||
async fn load_id_map(map_path: &PathBuf, timeouts: &TimeoutConfig) -> Result<IdMap, DomainError> {
|
async fn load_id_map(map_path: &PathBuf, timeouts: &TimeoutConfig) -> Result<IdMap, DomainError> {
|
||||||
if map_path.exists() {
|
if map_path.exists() {
|
||||||
// Intentar leer con timeout para evitar bloqueos indefinidos
|
// Try to read with timeout to avoid indefinite blocking
|
||||||
let read_result = time::timeout(
|
let read_result = time::timeout(
|
||||||
timeouts.lock_timeout(),
|
timeouts.lock_timeout(),
|
||||||
fs::read_to_string(map_path)
|
fs::read_to_string(map_path)
|
||||||
@@ -127,10 +127,10 @@ impl IdMappingService {
|
|||||||
|
|
||||||
let content = read_result.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to read ID map from {}: {}", map_path.display(), e)))?;
|
let content = read_result.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to read ID map from {}: {}", map_path.display(), e)))?;
|
||||||
|
|
||||||
// Parsear el JSON
|
// Parse the JSON
|
||||||
match serde_json::from_str::<IdMap>(&content) {
|
match serde_json::from_str::<IdMap>(&content) {
|
||||||
Ok(mut map) => {
|
Ok(mut map) => {
|
||||||
// Reconstruir el mapa inverso si es necesario
|
// Rebuild the inverse map if necessary
|
||||||
if map.id_to_path.is_empty() && !map.path_to_id.is_empty() {
|
if map.id_to_path.is_empty() && !map.path_to_id.is_empty() {
|
||||||
let mut rebuild_count = 0;
|
let mut rebuild_count = 0;
|
||||||
for (path, id) in &map.path_to_id {
|
for (path, id) in &map.path_to_id {
|
||||||
@@ -146,7 +146,7 @@ impl IdMappingService {
|
|||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Error parsing ID map: {}", e);
|
tracing::error!("Error parsing ID map: {}", e);
|
||||||
// Intentar hacer un respaldo del archivo corrupto
|
// Try to backup the corrupted file
|
||||||
let backup_path = map_path.with_extension("json.bak");
|
let backup_path = map_path.with_extension("json.bak");
|
||||||
if let Err(copy_err) = tokio::fs::copy(map_path, &backup_path).await {
|
if let Err(copy_err) = tokio::fs::copy(map_path, &backup_path).await {
|
||||||
tracing::error!("Failed to backup corrupted map file: {}", copy_err);
|
tracing::error!("Failed to backup corrupted map file: {}", copy_err);
|
||||||
@@ -158,18 +158,18 @@ impl IdMappingService {
|
|||||||
return Ok(IdMap {
|
return Ok(IdMap {
|
||||||
path_to_id: HashMap::new(),
|
path_to_id: HashMap::new(),
|
||||||
id_to_path: HashMap::new(),
|
id_to_path: HashMap::new(),
|
||||||
version: 1, // Iniciar con versión 1
|
version: 1, // Start with version 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Devolver un mapa vacío si el archivo no existe y crear el archivo
|
// Return an empty map if the file doesn't exist and create the file
|
||||||
tracing::info!("No existing ID map found, creating new empty map");
|
tracing::info!("No existing ID map found, creating new empty map");
|
||||||
let empty_map = IdMap {
|
let empty_map = IdMap {
|
||||||
path_to_id: HashMap::new(),
|
path_to_id: HashMap::new(),
|
||||||
id_to_path: HashMap::new(),
|
id_to_path: HashMap::new(),
|
||||||
version: 1, // Iniciar con versión 1
|
version: 1, // Start with version 1
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
@@ -198,16 +198,16 @@ impl IdMappingService {
|
|||||||
Ok(empty_map)
|
Ok(empty_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Guarda el mapa de IDs en disco de manera segura
|
/// Saves the ID map to disk safely
|
||||||
async fn save_id_map(&self) -> Result<(), DomainError> {
|
async fn save_id_map(&self) -> Result<(), DomainError> {
|
||||||
// Adquirir bloqueo exclusivo para salvar
|
// Acquire exclusive lock for saving
|
||||||
let _lock = time::timeout(
|
let _lock = time::timeout(
|
||||||
self.timeouts.lock_timeout(),
|
self.timeouts.lock_timeout(),
|
||||||
self.save_mutex.lock()
|
self.save_mutex.lock()
|
||||||
).await
|
).await
|
||||||
.map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring save lock for ID mapping"))?;
|
.map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring save lock for ID mapping"))?;
|
||||||
|
|
||||||
// Crear JSON con el lock de lectura para minimizar el tiempo de bloqueo
|
// Create JSON with read lock to minimize lock hold time
|
||||||
let json = {
|
let json = {
|
||||||
let mut map = time::timeout(
|
let mut map = time::timeout(
|
||||||
self.timeouts.lock_timeout(),
|
self.timeouts.lock_timeout(),
|
||||||
@@ -215,7 +215,7 @@ impl IdMappingService {
|
|||||||
).await
|
).await
|
||||||
.map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring write lock for ID mapping"))?;
|
.map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring write lock for ID mapping"))?;
|
||||||
|
|
||||||
// Incrementar versión sólo si hay cambios por guardar
|
// Increment version only if there are pending changes to save
|
||||||
let pending = *self.pending_save.read().await;
|
let pending = *self.pending_save.read().await;
|
||||||
if pending {
|
if pending {
|
||||||
map.version += 1;
|
map.version += 1;
|
||||||
@@ -227,16 +227,16 @@ impl IdMappingService {
|
|||||||
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to serialize ID map to JSON: {}", e)))?
|
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to serialize ID map to JSON: {}", e)))?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Escribir a un archivo temporal primero para evitar corrupción
|
// Write to a temporary file first to avoid corruption
|
||||||
let temp_path = self.map_path.with_extension("json.tmp");
|
let temp_path = self.map_path.with_extension("json.tmp");
|
||||||
fs::write(&temp_path, &json).await
|
fs::write(&temp_path, &json).await
|
||||||
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to write temporary ID map to {}: {}", temp_path.display(), e)))?;
|
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to write temporary ID map to {}: {}", temp_path.display(), e)))?;
|
||||||
|
|
||||||
// Realizar el rename atómico
|
// Perform the atomic rename
|
||||||
fs::rename(&temp_path, &self.map_path).await
|
fs::rename(&temp_path, &self.map_path).await
|
||||||
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to rename temporary ID map to {}: {}", self.map_path.display(), e)))?;
|
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to rename temporary ID map to {}: {}", self.map_path.display(), e)))?;
|
||||||
|
|
||||||
// Resetear flag de pendientes
|
// Reset pending flag
|
||||||
{
|
{
|
||||||
let mut pending = self.pending_save.write().await;
|
let mut pending = self.pending_save.write().await;
|
||||||
*pending = false;
|
*pending = false;
|
||||||
@@ -246,22 +246,22 @@ impl IdMappingService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Genera un ID único
|
/// Generates a unique ID
|
||||||
fn generate_id(&self) -> String {
|
fn generate_id(&self) -> String {
|
||||||
Uuid::new_v4().to_string()
|
Uuid::new_v4().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marca cambios como pendientes
|
/// Marks changes as pending
|
||||||
async fn mark_pending(&self) {
|
async fn mark_pending(&self) {
|
||||||
let mut pending = self.pending_save.write().await;
|
let mut pending = self.pending_save.write().await;
|
||||||
*pending = true;
|
*pending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene el ID para una ruta o genera uno nuevo si no existe
|
/// Gets the ID for a path or generates a new one if it doesn't exist
|
||||||
pub async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, IdMappingError> {
|
pub async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, IdMappingError> {
|
||||||
let path_str = path.to_string();
|
let path_str = path.to_string();
|
||||||
|
|
||||||
// Primer intento con lock de lectura (más eficiente)
|
// First attempt with read lock (more efficient)
|
||||||
{
|
{
|
||||||
let read_result = match time::timeout(
|
let read_result = match time::timeout(
|
||||||
self.timeouts.lock_timeout(),
|
self.timeouts.lock_timeout(),
|
||||||
@@ -276,7 +276,7 @@ impl IdMappingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si no se encuentra, adquirir lock de escritura
|
// If not found, acquire write lock
|
||||||
let write_result = match time::timeout(
|
let write_result = match time::timeout(
|
||||||
self.timeouts.lock_timeout(),
|
self.timeouts.lock_timeout(),
|
||||||
self.id_map.write()
|
self.id_map.write()
|
||||||
@@ -287,18 +287,18 @@ impl IdMappingService {
|
|||||||
|
|
||||||
let mut map = write_result;
|
let mut map = write_result;
|
||||||
|
|
||||||
// Verificar nuevamente (podría haberse agregado mientras esperábamos el lock)
|
// Check again (it could have been added while we were waiting for the lock)
|
||||||
if let Some(id) = map.path_to_id.get(&path_str) {
|
if let Some(id) = map.path_to_id.get(&path_str) {
|
||||||
return Ok(id.clone());
|
return Ok(id.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generar un nuevo ID y almacenarlo
|
// Generate a new ID and store it
|
||||||
let id = self.generate_id();
|
let id = self.generate_id();
|
||||||
map.path_to_id.insert(path_str.clone(), id.clone());
|
map.path_to_id.insert(path_str.clone(), id.clone());
|
||||||
map.id_to_path.insert(id.clone(), path_str);
|
map.id_to_path.insert(id.clone(), path_str);
|
||||||
|
|
||||||
// Marcar como pendiente para guardar
|
// Mark as pending for saving
|
||||||
drop(map); // Liberar el write lock antes de adquirir otro
|
drop(map); // Release the write lock before acquiring another
|
||||||
self.mark_pending().await;
|
self.mark_pending().await;
|
||||||
|
|
||||||
tracing::debug!("Created new ID mapping: {} -> {}", path.to_string(), id);
|
tracing::debug!("Created new ID mapping: {} -> {}", path.to_string(), id);
|
||||||
@@ -306,7 +306,7 @@ impl IdMappingService {
|
|||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene una ruta por su ID con manejo de timeout
|
/// Gets a path by its ID with timeout handling
|
||||||
pub async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, IdMappingError> {
|
pub async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, IdMappingError> {
|
||||||
let read_result = match time::timeout(
|
let read_result = match time::timeout(
|
||||||
self.timeouts.lock_timeout(),
|
self.timeouts.lock_timeout(),
|
||||||
@@ -323,7 +323,7 @@ impl IdMappingService {
|
|||||||
Err(IdMappingError::NotFound(id.to_string()))
|
Err(IdMappingError::NotFound(id.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza el mapeo de un ID existente a una nueva ruta
|
/// Updates the mapping of an existing ID to a new path
|
||||||
pub async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), IdMappingError> {
|
pub async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), IdMappingError> {
|
||||||
let write_result = match time::timeout(
|
let write_result = match time::timeout(
|
||||||
self.timeouts.lock_timeout(),
|
self.timeouts.lock_timeout(),
|
||||||
@@ -335,17 +335,17 @@ impl IdMappingService {
|
|||||||
|
|
||||||
let mut map = write_result;
|
let mut map = write_result;
|
||||||
|
|
||||||
// Buscar la ruta anterior para eliminarla
|
// Find the previous path to remove it
|
||||||
if let Some(old_path) = map.id_to_path.get(id).cloned() {
|
if let Some(old_path) = map.id_to_path.get(id).cloned() {
|
||||||
map.path_to_id.remove(&old_path);
|
map.path_to_id.remove(&old_path);
|
||||||
|
|
||||||
// Registrar la nueva ruta
|
// Register the new path
|
||||||
let new_path_str = new_path.to_string();
|
let new_path_str = new_path.to_string();
|
||||||
map.path_to_id.insert(new_path_str.clone(), id.to_string());
|
map.path_to_id.insert(new_path_str.clone(), id.to_string());
|
||||||
map.id_to_path.insert(id.to_string(), new_path_str);
|
map.id_to_path.insert(id.to_string(), new_path_str);
|
||||||
|
|
||||||
// Marcar como pendiente
|
// Mark as pending
|
||||||
drop(map); // Liberar el write lock antes de adquirir otro
|
drop(map); // Release the write lock before acquiring another
|
||||||
self.mark_pending().await;
|
self.mark_pending().await;
|
||||||
|
|
||||||
tracing::debug!("Updated path mapping for ID {}: {} -> {}",
|
tracing::debug!("Updated path mapping for ID {}: {} -> {}",
|
||||||
@@ -357,7 +357,7 @@ impl IdMappingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina un ID del mapa
|
/// Removes an ID from the map
|
||||||
pub async fn remove_id(&self, id: &str) -> Result<(), IdMappingError> {
|
pub async fn remove_id(&self, id: &str) -> Result<(), IdMappingError> {
|
||||||
let write_result = match time::timeout(
|
let write_result = match time::timeout(
|
||||||
self.timeouts.lock_timeout(),
|
self.timeouts.lock_timeout(),
|
||||||
@@ -369,12 +369,12 @@ impl IdMappingService {
|
|||||||
|
|
||||||
let mut map = write_result;
|
let mut map = write_result;
|
||||||
|
|
||||||
// Buscar la ruta para eliminarla
|
// Find the path to remove it
|
||||||
if let Some(path) = map.id_to_path.remove(id) {
|
if let Some(path) = map.id_to_path.remove(id) {
|
||||||
map.path_to_id.remove(&path);
|
map.path_to_id.remove(&path);
|
||||||
|
|
||||||
// Marcar como pendiente
|
// Mark as pending
|
||||||
drop(map); // Liberar el write lock antes de adquirir otro
|
drop(map); // Release the write lock before acquiring another
|
||||||
self.mark_pending().await;
|
self.mark_pending().await;
|
||||||
|
|
||||||
tracing::debug!("Removed ID mapping: {} -> {}", id, path);
|
tracing::debug!("Removed ID mapping: {} -> {}", id, path);
|
||||||
@@ -384,9 +384,9 @@ impl IdMappingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Guarda cambios pendientes al disco inmediatamente, sin debounce
|
/// Saves pending changes to disk immediately, without debounce
|
||||||
pub async fn save_pending_changes(&self) -> Result<(), IdMappingError> {
|
pub async fn save_pending_changes(&self) -> Result<(), IdMappingError> {
|
||||||
// Verificar si hay cambios pendientes
|
// Check if there are pending changes
|
||||||
{
|
{
|
||||||
let pending = self.pending_save.read().await;
|
let pending = self.pending_save.read().await;
|
||||||
if !*pending {
|
if !*pending {
|
||||||
@@ -394,12 +394,12 @@ impl IdMappingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guardar inmediatamente (sin debounce ni spawn)
|
// Save immediately (without debounce or spawn)
|
||||||
match self.save_id_map().await {
|
match self.save_id_map().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!("ID mappings saved successfully to disk at {}", self.map_path.display());
|
tracing::info!("ID mappings saved successfully to disk at {}", self.map_path.display());
|
||||||
|
|
||||||
// Verificar explícitamente que el archivo existe y tiene tamaño
|
// Explicitly verify that the file exists and has size
|
||||||
match std::fs::metadata(&self.map_path) {
|
match std::fs::metadata(&self.map_path) {
|
||||||
Ok(metadata) => {
|
Ok(metadata) => {
|
||||||
if metadata.len() > 0 {
|
if metadata.len() > 0 {
|
||||||
@@ -410,7 +410,7 @@ impl IdMappingService {
|
|||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to verify saved map file: {}", e);
|
tracing::error!("Failed to verify saved map file: {}", e);
|
||||||
// Intentar un segundo guardado si la verificación falla
|
// Try a second save if verification fails
|
||||||
if let Err(retry_err) = self.save_id_map().await {
|
if let Err(retry_err) = self.save_id_map().await {
|
||||||
tracing::error!("Second save attempt also failed: {}", retry_err);
|
tracing::error!("Second save attempt also failed: {}", retry_err);
|
||||||
return Err(IdMappingError::IoError(std::io::Error::new(
|
return Err(IdMappingError::IoError(std::io::Error::new(
|
||||||
@@ -426,7 +426,7 @@ impl IdMappingService {
|
|||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to save ID map to {}: {}", self.map_path.display(), e);
|
tracing::error!("Failed to save ID map to {}: {}", self.map_path.display(), e);
|
||||||
// Intentar un segundo guardado con retraso en caso de error
|
// Try a second save with delay in case of error
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
match self.save_id_map().await {
|
match self.save_id_map().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -448,31 +448,31 @@ impl IdMappingService {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl IdMappingPort for IdMappingService {
|
impl IdMappingPort for IdMappingService {
|
||||||
/// Obtiene el ID para una ruta o genera uno nuevo si no existe
|
/// Gets the ID for a path or generates a new one if it doesn't exist
|
||||||
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> {
|
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> {
|
||||||
self.get_or_create_id(path).await
|
self.get_or_create_id(path).await
|
||||||
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get or create ID for path: {}: {}", path.to_string(), e)))
|
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get or create ID for path: {}: {}", path.to_string(), e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene una ruta por su ID con manejo de timeout
|
/// Gets a path by its ID with timeout handling
|
||||||
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError> {
|
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError> {
|
||||||
self.get_path_by_id(id).await
|
self.get_path_by_id(id).await
|
||||||
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get path for ID: {}: {}", id, e)))
|
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get path for ID: {}: {}", id, e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actualiza el mapeo de un ID existente a una nueva ruta
|
/// Updates the mapping of an existing ID to a new path
|
||||||
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> {
|
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> {
|
||||||
self.update_path(id, new_path).await
|
self.update_path(id, new_path).await
|
||||||
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to update path for ID: {} to {}: {}", id, new_path.to_string(), e)))
|
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to update path for ID: {} to {}: {}", id, new_path.to_string(), e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina un ID del mapa
|
/// Removes an ID from the map
|
||||||
async fn remove_id(&self, id: &str) -> Result<(), DomainError> {
|
async fn remove_id(&self, id: &str) -> Result<(), DomainError> {
|
||||||
self.remove_id(id).await
|
self.remove_id(id).await
|
||||||
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to remove ID: {}: {}", id, e)))
|
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to remove ID: {}: {}", id, e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Guarda cambios pendientes al disco
|
/// Saves pending changes to disk
|
||||||
async fn save_changes(&self) -> Result<(), DomainError> {
|
async fn save_changes(&self) -> Result<(), DomainError> {
|
||||||
self.save_pending_changes().await
|
self.save_pending_changes().await
|
||||||
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to save pending ID mapping changes: {}", e)))
|
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to save pending ID mapping changes: {}", e)))
|
||||||
@@ -481,7 +481,7 @@ impl IdMappingPort for IdMappingService {
|
|||||||
|
|
||||||
// The extension methods were moved to the IdMappingPort trait as default implementations
|
// The extension methods were moved to the IdMappingPort trait as default implementations
|
||||||
|
|
||||||
// Implementar Clone para poder usar en tokio::spawn
|
// Implement Clone to allow use in tokio::spawn
|
||||||
/// Synchronous helper for contexts where we can't use async
|
/// Synchronous helper for contexts where we can't use async
|
||||||
impl IdMappingService {
|
impl IdMappingService {
|
||||||
/// Create a new service synchronously (only for stubs and initialization)
|
/// Create a new service synchronously (only for stubs and initialization)
|
||||||
@@ -499,13 +499,13 @@ impl IdMappingService {
|
|||||||
|
|
||||||
impl Clone for IdMappingService {
|
impl Clone for IdMappingService {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
// No podemos clonar directamente los RwLock/Mutex,
|
// We cannot directly clone the RwLock/Mutex,
|
||||||
// pero podemos crear nuevas instancias que apunten al mismo Arc interno
|
// but we can create new instances that point to the same internal Arc
|
||||||
// Sin embargo, en este caso simplemente necesitamos la map_path
|
// However, in this case we simply need the map_path
|
||||||
Self {
|
Self {
|
||||||
map_path: self.map_path.clone(),
|
map_path: self.map_path.clone(),
|
||||||
id_map: RwLock::new(IdMap::default()), // Esto no se usa en el task asíncrono
|
id_map: RwLock::new(IdMap::default()), // This is not used in the async task
|
||||||
save_mutex: Mutex::new(()), // Esto tampoco
|
save_mutex: Mutex::new(()), // Neither is this
|
||||||
timeouts: self.timeouts.clone(),
|
timeouts: self.timeouts.clone(),
|
||||||
pending_save: RwLock::new(false),
|
pending_save: RwLock::new(false),
|
||||||
}
|
}
|
||||||
@@ -530,7 +530,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(!id.is_empty(), "ID should not be empty");
|
assert!(!id.is_empty(), "ID should not be empty");
|
||||||
|
|
||||||
// Verificar que el mismo ID se devuelve para la misma ruta
|
// Verify that the same ID is returned for the same path
|
||||||
let id2 = service.get_or_create_id(&path).await.unwrap();
|
let id2 = service.get_or_create_id(&path).await.unwrap();
|
||||||
assert_eq!(id, id2, "Same path should return same ID");
|
assert_eq!(id, id2, "Same path should return same ID");
|
||||||
}
|
}
|
||||||
@@ -557,7 +557,7 @@ mod tests {
|
|||||||
let temp_dir = tempdir().unwrap();
|
let temp_dir = tempdir().unwrap();
|
||||||
let map_path = temp_dir.path().join("id_map.json");
|
let map_path = temp_dir.path().join("id_map.json");
|
||||||
|
|
||||||
// Crear y poblar el servicio
|
// Create and populate the service
|
||||||
let service = IdMappingService::new(map_path.clone()).await.unwrap();
|
let service = IdMappingService::new(map_path.clone()).await.unwrap();
|
||||||
|
|
||||||
let path1 = StoragePath::from_string("/test/file1.txt");
|
let path1 = StoragePath::from_string("/test/file1.txt");
|
||||||
@@ -565,16 +565,16 @@ mod tests {
|
|||||||
let id1 = service.get_or_create_id(&path1).await.unwrap();
|
let id1 = service.get_or_create_id(&path1).await.unwrap();
|
||||||
let id2 = service.get_or_create_id(&path2).await.unwrap();
|
let id2 = service.get_or_create_id(&path2).await.unwrap();
|
||||||
|
|
||||||
// Guardar cambios
|
// Save changes
|
||||||
service.save_pending_changes().await.unwrap();
|
service.save_pending_changes().await.unwrap();
|
||||||
|
|
||||||
// Esperar para asegurar que el guardado asíncrono termine
|
// Wait to ensure the async save completes
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
|
||||||
// Crear un nuevo servicio que debería cargar el mismo mapa
|
// Create a new service that should load the same map
|
||||||
let service2 = IdMappingService::new(map_path).await.unwrap();
|
let service2 = IdMappingService::new(map_path).await.unwrap();
|
||||||
|
|
||||||
// Verificar que los IDs coinciden
|
// Verify that the IDs match
|
||||||
let loaded_id1 = service2.get_or_create_id(&path1).await.unwrap();
|
let loaded_id1 = service2.get_or_create_id(&path1).await.unwrap();
|
||||||
let loaded_id2 = service2.get_or_create_id(&path2).await.unwrap();
|
let loaded_id2 = service2.get_or_create_id(&path2).await.unwrap();
|
||||||
|
|
||||||
@@ -591,7 +591,7 @@ mod tests {
|
|||||||
|
|
||||||
let service = std::sync::Arc::new(IdMappingService::new(map_path).await.unwrap());
|
let service = std::sync::Arc::new(IdMappingService::new(map_path).await.unwrap());
|
||||||
|
|
||||||
// Crear múltiples tareas que intentan acceder simultáneamente
|
// Create multiple tasks that attempt simultaneous access
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
for i in 0..100 {
|
for i in 0..100 {
|
||||||
let path = StoragePath::from_string(&format!("/test/concurrent/file{}.txt", i));
|
let path = StoragePath::from_string(&format!("/test/concurrent/file{}.txt", i));
|
||||||
@@ -602,15 +602,15 @@ mod tests {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Esperar a que todas terminen
|
// Wait for all to finish
|
||||||
let results = join_all(tasks).await;
|
let results = join_all(tasks).await;
|
||||||
|
|
||||||
// Verificar que todas tuvieron éxito
|
// Verify that all succeeded
|
||||||
for result in results {
|
for result in results {
|
||||||
assert!(result.unwrap().is_ok(), "Concurrent operations should succeed");
|
assert!(result.unwrap().is_ok(), "Concurrent operations should succeed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guardar cambios
|
// Save changes
|
||||||
service.save_pending_changes().await.unwrap();
|
service.save_pending_changes().await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ impl TokenServicePort for JwtTokenService {
|
|||||||
DomainError::new(
|
DomainError::new(
|
||||||
ErrorKind::InternalError,
|
ErrorKind::InternalError,
|
||||||
"TokenService",
|
"TokenService",
|
||||||
format!("Error al generar token: {}", e)
|
format!("Error generating token: {}", e)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -126,12 +126,12 @@ impl TokenServicePort for JwtTokenService {
|
|||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
match e.kind() {
|
match e.kind() {
|
||||||
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
|
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
|
||||||
DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expirado")
|
DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expired")
|
||||||
},
|
},
|
||||||
_ => DomainError::new(
|
_ => DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"TokenService",
|
"TokenService",
|
||||||
format!("Token inválido: {}", e)
|
format!("Invalid token: {}", e)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//! PathService - Servicio de infraestructura para manejo de rutas de almacenamiento
|
//! PathService - Infrastructure service for storage path management
|
||||||
//!
|
//!
|
||||||
//! Este servicio fue movido desde domain/services porque implementa traits de application
|
//! This service was moved from domain/services because it implements application traits
|
||||||
//! (StoragePort, StorageMediator) y tiene dependencias de sistema de archivos (tokio::fs).
|
//! (StoragePort, StorageMediator) and has file system dependencies (tokio::fs).
|
||||||
//!
|
//!
|
||||||
//! StoragePath (Value Object) permanece en domain/services/path_service.rs
|
//! StoragePath (Value Object) remains in domain/services/path_service.rs
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -15,18 +15,18 @@ use crate::application::services::storage_mediator::{StorageMediator, StorageMed
|
|||||||
use crate::domain::entities::folder::Folder;
|
use crate::domain::entities::folder::Folder;
|
||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::StoragePath;
|
||||||
|
|
||||||
/// Servicio de infraestructura para manejar operaciones con rutas de almacenamiento
|
/// Infrastructure service for handling storage path operations
|
||||||
pub struct PathService {
|
pub struct PathService {
|
||||||
root_path: PathBuf,
|
root_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PathService {
|
impl PathService {
|
||||||
/// Crea un nuevo servicio de rutas con una raíz específica
|
/// Creates a new path service with a specific root
|
||||||
pub fn new(root_path: PathBuf) -> Self {
|
pub fn new(root_path: PathBuf) -> Self {
|
||||||
Self { root_path }
|
Self { root_path }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convierte una ruta del dominio a una ruta física absoluta
|
/// Converts a domain path to an absolute physical path
|
||||||
pub fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
|
pub fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||||
let mut path = self.root_path.clone();
|
let mut path = self.root_path.clone();
|
||||||
for segment in storage_path.segments() {
|
for segment in storage_path.segments() {
|
||||||
@@ -35,7 +35,7 @@ impl PathService {
|
|||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convierte una ruta física a una ruta de dominio
|
/// Converts a physical path to a domain path
|
||||||
pub fn to_storage_path(&self, physical_path: &Path) -> Option<StoragePath> {
|
pub fn to_storage_path(&self, physical_path: &Path) -> Option<StoragePath> {
|
||||||
physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| {
|
physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| {
|
||||||
let segments: Vec<String> = rel_path
|
let segments: Vec<String> = rel_path
|
||||||
@@ -49,12 +49,12 @@ impl PathService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una ruta de archivo dentro de una carpeta
|
/// Creates a file path within a folder
|
||||||
pub fn create_file_path(&self, folder_path: &StoragePath, file_name: &str) -> StoragePath {
|
pub fn create_file_path(&self, folder_path: &StoragePath, file_name: &str) -> StoragePath {
|
||||||
folder_path.join(file_name)
|
folder_path.join(file_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifica si una ruta es directamente hija de otra
|
/// Checks if a path is a direct child of another
|
||||||
pub fn is_direct_child(&self, parent_path: &StoragePath, potential_child: &StoragePath) -> bool {
|
pub fn is_direct_child(&self, parent_path: &StoragePath, potential_child: &StoragePath) -> bool {
|
||||||
if let Some(child_parent) = potential_child.parent() {
|
if let Some(child_parent) = potential_child.parent() {
|
||||||
&child_parent == parent_path
|
&child_parent == parent_path
|
||||||
@@ -63,7 +63,7 @@ impl PathService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifica si una ruta está en la raíz
|
/// Checks if a path is at the root
|
||||||
pub fn is_in_root(&self, path: &StoragePath) -> bool {
|
pub fn is_in_root(&self, path: &StoragePath) -> bool {
|
||||||
path.parent().map_or(true, |p| p.is_empty())
|
path.parent().map_or(true, |p| p.is_empty())
|
||||||
}
|
}
|
||||||
@@ -73,9 +73,9 @@ impl PathService {
|
|||||||
&self.root_path
|
&self.root_path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Valida una ruta para asegurar que no contiene componentes peligrosos
|
/// Validates a path to ensure it doesn't contain dangerous components
|
||||||
pub fn validate_path(&self, path: &StoragePath) -> Result<(), DomainError> {
|
pub fn validate_path(&self, path: &StoragePath) -> Result<(), DomainError> {
|
||||||
// Verificar que no haya segmentos vacíos
|
// Check for empty segments
|
||||||
if path.segments().iter().any(|s| s.is_empty()) {
|
if path.segments().iter().any(|s| s.is_empty()) {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::InvalidInput,
|
ErrorKind::InvalidInput,
|
||||||
@@ -84,7 +84,7 @@ impl PathService {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar que no haya caracteres peligrosos
|
// Check for dangerous characters
|
||||||
let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
|
let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
|
||||||
for segment in path.segments() {
|
for segment in path.segments() {
|
||||||
if segment.contains(&dangerous_chars[..]) {
|
if segment.contains(&dangerous_chars[..]) {
|
||||||
@@ -95,7 +95,7 @@ impl PathService {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar que no empiece con . (oculto en Unix)
|
// Check that it doesn't start with . (hidden in Unix)
|
||||||
if segment.starts_with('.') && segment != ".well-known" {
|
if segment.starts_with('.') && segment != ".well-known" {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::InvalidInput,
|
ErrorKind::InvalidInput,
|
||||||
@@ -120,13 +120,13 @@ impl StoragePort for PathService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> {
|
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> {
|
||||||
// Primero validar la ruta
|
// First validate the path
|
||||||
self.validate_path(storage_path)?;
|
self.validate_path(storage_path)?;
|
||||||
|
|
||||||
// Resolver a ruta física
|
// Resolve to physical path
|
||||||
let physical_path = self.resolve_path(storage_path);
|
let physical_path = self.resolve_path(storage_path);
|
||||||
|
|
||||||
// Crear directorios si no existen
|
// Create directories if they don't exist
|
||||||
if !physical_path.exists() {
|
if !physical_path.exists() {
|
||||||
fs::create_dir_all(&physical_path).await
|
fs::create_dir_all(&physical_path).await
|
||||||
.map_err(|e| DomainError::new(
|
.map_err(|e| DomainError::new(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::common::errors::Result;
|
|||||||
use crate::domain::repositories::trash_repository::TrashRepository;
|
use crate::domain::repositories::trash_repository::TrashRepository;
|
||||||
use crate::application::ports::trash_ports::TrashUseCase;
|
use crate::application::ports::trash_ports::TrashUseCase;
|
||||||
|
|
||||||
/// Servicio para la limpieza automática de elementos expirados en la papelera
|
/// Service for automatic cleanup of expired items in the trash
|
||||||
pub struct TrashCleanupService {
|
pub struct TrashCleanupService {
|
||||||
trash_service: Arc<dyn TrashUseCase>,
|
trash_service: Arc<dyn TrashUseCase>,
|
||||||
trash_repository: Arc<dyn TrashRepository>,
|
trash_repository: Arc<dyn TrashRepository>,
|
||||||
@@ -23,75 +23,75 @@ impl TrashCleanupService {
|
|||||||
Self {
|
Self {
|
||||||
trash_service,
|
trash_service,
|
||||||
trash_repository,
|
trash_repository,
|
||||||
cleanup_interval_hours: cleanup_interval_hours.max(1), // Mínimo 1 hora
|
cleanup_interval_hours: cleanup_interval_hours.max(1), // Minimum 1 hour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inicia el trabajo de limpieza periódica
|
/// Starts the periodic cleanup job
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn start_cleanup_job(&self) {
|
pub async fn start_cleanup_job(&self) {
|
||||||
let trash_repository = self.trash_repository.clone();
|
let trash_repository = self.trash_repository.clone();
|
||||||
let trash_service = self.trash_service.clone();
|
let trash_service = self.trash_service.clone();
|
||||||
let interval_hours = self.cleanup_interval_hours;
|
let interval_hours = self.cleanup_interval_hours;
|
||||||
|
|
||||||
info!("Iniciando trabajo de limpieza de papelera con intervalo de {} horas", interval_hours);
|
info!("Starting trash cleanup job with interval of {} hours", interval_hours);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let interval_duration = Duration::from_secs(interval_hours * 60 * 60);
|
let interval_duration = Duration::from_secs(interval_hours * 60 * 60);
|
||||||
let mut interval = time::interval(interval_duration);
|
let mut interval = time::interval(interval_duration);
|
||||||
|
|
||||||
// Primera ejecución inmediata
|
// First immediate execution
|
||||||
Self::cleanup_expired_items(trash_repository.clone(), trash_service.clone()).await
|
Self::cleanup_expired_items(trash_repository.clone(), trash_service.clone()).await
|
||||||
.unwrap_or_else(|e| error!("Error en la limpieza inicial de la papelera: {:?}", e));
|
.unwrap_or_else(|e| error!("Error in initial trash cleanup: {:?}", e));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
debug!("Ejecutando tarea programada de limpieza de papelera");
|
debug!("Running scheduled trash cleanup task");
|
||||||
|
|
||||||
if let Err(e) = Self::cleanup_expired_items(
|
if let Err(e) = Self::cleanup_expired_items(
|
||||||
trash_repository.clone(),
|
trash_repository.clone(),
|
||||||
trash_service.clone()
|
trash_service.clone()
|
||||||
).await {
|
).await {
|
||||||
error!("Error en la limpieza programada de la papelera: {:?}", e);
|
error!("Error in scheduled trash cleanup: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Limpia los elementos expirados en la papelera
|
/// Cleans up expired items in the trash
|
||||||
#[instrument(skip(trash_repository, trash_service))]
|
#[instrument(skip(trash_repository, trash_service))]
|
||||||
async fn cleanup_expired_items(
|
async fn cleanup_expired_items(
|
||||||
trash_repository: Arc<dyn TrashRepository>,
|
trash_repository: Arc<dyn TrashRepository>,
|
||||||
trash_service: Arc<dyn TrashUseCase>,
|
trash_service: Arc<dyn TrashUseCase>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
debug!("Comenzando limpieza de elementos expirados en la papelera");
|
debug!("Starting cleanup of expired items in the trash");
|
||||||
|
|
||||||
// Obtener todos los elementos expirados
|
// Get all expired items
|
||||||
let expired_items = trash_repository.get_expired_items().await?;
|
let expired_items = trash_repository.get_expired_items().await?;
|
||||||
|
|
||||||
if expired_items.is_empty() {
|
if expired_items.is_empty() {
|
||||||
debug!("No hay elementos expirados para limpiar");
|
debug!("No expired items to clean up");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Encontrados {} elementos expirados para eliminar", expired_items.len());
|
info!("Found {} expired items to delete", expired_items.len());
|
||||||
|
|
||||||
// Eliminar cada elemento expirado
|
// Delete each expired item
|
||||||
for item in expired_items {
|
for item in expired_items {
|
||||||
let trash_id = item.id().to_string();
|
let trash_id = item.id().to_string();
|
||||||
let user_id = item.user_id().to_string();
|
let user_id = item.user_id().to_string();
|
||||||
|
|
||||||
debug!("Eliminando elemento expirado: id={}, user={}", trash_id, user_id);
|
debug!("Deleting expired item: id={}, user={}", trash_id, user_id);
|
||||||
|
|
||||||
// Si falla una eliminación, continuar con las demás
|
// If a deletion fails, continue with the rest
|
||||||
if let Err(e) = trash_service.delete_permanently(&trash_id, &user_id).await {
|
if let Err(e) = trash_service.delete_permanently(&trash_id, &user_id).await {
|
||||||
error!("Error eliminando elemento expirado {}: {:?}", trash_id, e);
|
error!("Error deleting expired item {}: {:?}", trash_id, e);
|
||||||
} else {
|
} else {
|
||||||
debug!("Elemento expirado eliminado correctamente: {}", trash_id);
|
debug!("Expired item deleted successfully: {}", trash_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Limpieza de papelera completada");
|
info!("Trash cleanup completed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,47 +13,47 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Error relacionado con la creación de archivos ZIP
|
/// Error related to ZIP file creation
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ZipError {
|
pub enum ZipError {
|
||||||
#[error("Error de IO: {0}")]
|
#[error("IO error: {0}")]
|
||||||
IoError(#[from] std::io::Error),
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("Error de ZIP: {0}")]
|
#[error("ZIP error: {0}")]
|
||||||
ZipError(#[from] zip::result::ZipError),
|
ZipError(#[from] zip::result::ZipError),
|
||||||
|
|
||||||
#[error("Error al leer el archivo: {0}")]
|
#[error("Error reading file: {0}")]
|
||||||
FileReadError(String),
|
FileReadError(String),
|
||||||
|
|
||||||
#[error("Error al obtener contenido de carpeta: {0}")]
|
#[error("Error getting folder contents: {0}")]
|
||||||
FolderContentsError(String),
|
FolderContentsError(String),
|
||||||
|
|
||||||
#[error("Carpeta no encontrada: {0}")]
|
#[error("Folder not found: {0}")]
|
||||||
FolderNotFound(String),
|
FolderNotFound(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementar From<ZipError> para DomainError para permitir el uso de ?
|
// Implement From<ZipError> for DomainError to allow the use of ?
|
||||||
impl From<ZipError> for DomainError {
|
impl From<ZipError> for DomainError {
|
||||||
fn from(err: ZipError) -> Self {
|
fn from(err: ZipError) -> Self {
|
||||||
DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string())
|
DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementar From<zip::result::ZipError> para DomainError directamente
|
// Implement From<zip::result::ZipError> for DomainError directly
|
||||||
impl From<zip::result::ZipError> for DomainError {
|
impl From<zip::result::ZipError> for DomainError {
|
||||||
fn from(err: zip::result::ZipError) -> Self {
|
fn from(err: zip::result::ZipError) -> Self {
|
||||||
DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string())
|
DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Servicio para crear archivos ZIP
|
/// Service for creating ZIP files
|
||||||
pub struct ZipService {
|
pub struct ZipService {
|
||||||
file_service: Arc<dyn FileRetrievalUseCase>,
|
file_service: Arc<dyn FileRetrievalUseCase>,
|
||||||
folder_service: Arc<dyn FolderUseCase>,
|
folder_service: Arc<dyn FolderUseCase>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ZipService {
|
impl ZipService {
|
||||||
/// Crea una nueva instancia del servicio ZIP con una referencia al servicio de archivos
|
/// Creates a new instance of the ZIP service with a reference to the file service
|
||||||
pub fn new(file_service: Arc<dyn FileRetrievalUseCase>, folder_service: Arc<dyn FolderUseCase>) -> Self {
|
pub fn new(file_service: Arc<dyn FileRetrievalUseCase>, folder_service: Arc<dyn FolderUseCase>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
file_service,
|
file_service,
|
||||||
@@ -61,33 +61,33 @@ impl ZipService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un archivo ZIP con el contenido de una carpeta y todas sus subcarpetas
|
/// Creates a ZIP file with the contents of a folder and all its subfolders
|
||||||
/// Retorna los bytes del ZIP
|
/// Returns the ZIP bytes
|
||||||
pub async fn create_folder_zip(&self, folder_id: &str, folder_name: &str) -> Result<Vec<u8>> {
|
pub async fn create_folder_zip(&self, folder_id: &str, folder_name: &str) -> Result<Vec<u8>> {
|
||||||
info!("Creando ZIP para carpeta: {} (ID: {})", folder_name, folder_id);
|
info!("Creating ZIP for folder: {} (ID: {})", folder_name, folder_id);
|
||||||
|
|
||||||
// Verificar si la carpeta existe
|
// Verify if the folder exists
|
||||||
let folder = match self.folder_service.get_folder(folder_id).await {
|
let folder = match self.folder_service.get_folder(folder_id).await {
|
||||||
Ok(folder) => folder,
|
Ok(folder) => folder,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al obtener carpeta {}: {}", folder_id, e);
|
error!("Error getting folder {}: {}", folder_id, e);
|
||||||
return Err(ZipError::FolderNotFound(folder_id.to_string()).into());
|
return Err(ZipError::FolderNotFound(folder_id.to_string()).into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Crear un buffer en memoria para el ZIP
|
// Create an in-memory buffer for the ZIP
|
||||||
let buf = Cursor::new(Vec::new());
|
let buf = Cursor::new(Vec::new());
|
||||||
let mut zip = ZipWriter::new(buf);
|
let mut zip = ZipWriter::new(buf);
|
||||||
|
|
||||||
// Establecer opciones de compresión
|
// Set compression options
|
||||||
let options = SimpleFileOptions::default()
|
let options = SimpleFileOptions::default()
|
||||||
.compression_method(zip::CompressionMethod::Deflated)
|
.compression_method(zip::CompressionMethod::Deflated)
|
||||||
.unix_permissions(0o755);
|
.unix_permissions(0o755);
|
||||||
|
|
||||||
// Objeto para seguir las carpetas procesadas y evitar ciclos
|
// Object to track processed folders and avoid cycles
|
||||||
let mut processed_folders = std::collections::HashSet::new();
|
let mut processed_folders = std::collections::HashSet::new();
|
||||||
|
|
||||||
// Procesamos la carpeta raíz y construimos el ZIP
|
// Process the root folder and build the ZIP
|
||||||
self.process_folder_recursively(
|
self.process_folder_recursively(
|
||||||
&mut zip,
|
&mut zip,
|
||||||
&folder,
|
&folder,
|
||||||
@@ -96,20 +96,20 @@ impl ZipService {
|
|||||||
&mut processed_folders
|
&mut processed_folders
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
// Finalizar el ZIP y obtener los bytes
|
// Finalize the ZIP and get the bytes
|
||||||
let mut zip_buf = zip.finish()?;
|
let mut zip_buf = zip.finish()?;
|
||||||
|
|
||||||
let mut bytes = Vec::new();
|
let mut bytes = Vec::new();
|
||||||
match zip_buf.read_to_end(&mut bytes) {
|
match zip_buf.read_to_end(&mut bytes) {
|
||||||
Ok(_) => Ok(bytes),
|
Ok(_) => Ok(bytes),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al leer ZIP finalizado: {}", e);
|
error!("Error reading finalized ZIP: {}", e);
|
||||||
Err(ZipError::IoError(e).into())
|
Err(ZipError::IoError(e).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementación alternativa para evitar recursión en async
|
// Alternative implementation to avoid recursion in async
|
||||||
async fn process_folder_recursively(
|
async fn process_folder_recursively(
|
||||||
&self,
|
&self,
|
||||||
zip: &mut ZipWriter<Cursor<Vec<u8>>>,
|
zip: &mut ZipWriter<Cursor<Vec<u8>>>,
|
||||||
@@ -118,63 +118,63 @@ impl ZipService {
|
|||||||
options: &SimpleFileOptions,
|
options: &SimpleFileOptions,
|
||||||
processed_folders: &mut std::collections::HashSet<String>
|
processed_folders: &mut std::collections::HashSet<String>
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Estructura para representar el trabajo pendiente
|
// Structure to represent pending work
|
||||||
struct PendingFolder {
|
struct PendingFolder {
|
||||||
folder: FolderDto,
|
folder: FolderDto,
|
||||||
path: String,
|
path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cola de trabajo para procesamiento iterativo
|
// Work queue for iterative processing
|
||||||
let mut work_queue = vec![PendingFolder {
|
let mut work_queue = vec![PendingFolder {
|
||||||
folder: folder.clone(),
|
folder: folder.clone(),
|
||||||
path: path.to_string(),
|
path: path.to_string(),
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Procesar la cola mientras haya elementos
|
// Process the queue while there are elements
|
||||||
while let Some(current) = work_queue.pop() {
|
while let Some(current) = work_queue.pop() {
|
||||||
let folder_id = current.folder.id.to_string();
|
let folder_id = current.folder.id.to_string();
|
||||||
|
|
||||||
// Evitar ciclos
|
// Avoid cycles
|
||||||
if processed_folders.contains(&folder_id) {
|
if processed_folders.contains(&folder_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
processed_folders.insert(folder_id.clone());
|
processed_folders.insert(folder_id.clone());
|
||||||
|
|
||||||
// Crear la entrada de directorio en el ZIP
|
// Create the directory entry in the ZIP
|
||||||
let folder_path = format!("{}/", current.path);
|
let folder_path = format!("{}/", current.path);
|
||||||
match zip.add_directory(&folder_path, *options) {
|
match zip.add_directory(&folder_path, *options) {
|
||||||
Ok(_) => debug!("Carpeta agregada al ZIP: {}", folder_path),
|
Ok(_) => debug!("Folder added to ZIP: {}", folder_path),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("No se pudo agregar carpeta al ZIP (puede que ya exista): {}", e);
|
warn!("Could not add folder to ZIP (it may already exist): {}", e);
|
||||||
// Continuamos aunque falle crear el directorio (podría estar duplicado)
|
// Continue even if creating the directory fails (it could be a duplicate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agregar archivos de la carpeta al ZIP
|
// Add files from the folder to the ZIP
|
||||||
let files = match self.file_service.list_files(Some(&folder_id)).await {
|
let files = match self.file_service.list_files(Some(&folder_id)).await {
|
||||||
Ok(files) => files,
|
Ok(files) => files,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al listar archivos en carpeta {}: {}", folder_id, e);
|
error!("Error listing files in folder {}: {}", folder_id, e);
|
||||||
return Err(ZipError::FolderContentsError(format!("Error al listar archivos: {}", e)).into());
|
return Err(ZipError::FolderContentsError(format!("Error listing files: {}", e)).into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Agregar cada archivo al ZIP
|
// Add each file to the ZIP
|
||||||
for file in files {
|
for file in files {
|
||||||
self.add_file_to_zip(zip, &file, &folder_path, options).await?;
|
self.add_file_to_zip(zip, &file, &folder_path, options).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Procesar subcarpetas
|
// Process subfolders
|
||||||
let subfolders = match self.folder_service.list_folders(Some(&folder_id)).await {
|
let subfolders = match self.folder_service.list_folders(Some(&folder_id)).await {
|
||||||
Ok(folders) => folders,
|
Ok(folders) => folders,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al listar subcarpetas en {}: {}", folder_id, e);
|
error!("Error listing subfolders in {}: {}", folder_id, e);
|
||||||
return Err(ZipError::FolderContentsError(format!("Error al listar subcarpetas: {}", e)).into());
|
return Err(ZipError::FolderContentsError(format!("Error listing subfolders: {}", e)).into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Agregar subcarpetas a la cola
|
// Add subfolders to the queue
|
||||||
for subfolder in subfolders {
|
for subfolder in subfolders {
|
||||||
let subfolder_path = format!("{}/{}", current.path, subfolder.name);
|
let subfolder_path = format!("{}/{}", current.path, subfolder.name);
|
||||||
work_queue.push(PendingFolder {
|
work_queue.push(PendingFolder {
|
||||||
@@ -187,7 +187,7 @@ impl ZipService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agrega un archivo al ZIP
|
// Adds a file to the ZIP
|
||||||
async fn add_file_to_zip(
|
async fn add_file_to_zip(
|
||||||
&self,
|
&self,
|
||||||
zip: &mut ZipWriter<Cursor<Vec<u8>>>,
|
zip: &mut ZipWriter<Cursor<Vec<u8>>>,
|
||||||
@@ -196,34 +196,34 @@ impl ZipService {
|
|||||||
options: &SimpleFileOptions,
|
options: &SimpleFileOptions,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let file_path = format!("{}{}", folder_path, file.name);
|
let file_path = format!("{}{}", folder_path, file.name);
|
||||||
info!("Agregando archivo al ZIP: {}", file_path);
|
info!("Adding file to ZIP: {}", file_path);
|
||||||
|
|
||||||
// Obtener el contenido del archivo
|
// Get the file content
|
||||||
let file_id = file.id.to_string();
|
let file_id = file.id.to_string();
|
||||||
let content = match self.file_service.get_file_content(&file_id).await {
|
let content = match self.file_service.get_file_content(&file_id).await {
|
||||||
Ok(content) => content,
|
Ok(content) => content,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al leer contenido del archivo {}: {}", file_id, e);
|
error!("Error reading file content {}: {}", file_id, e);
|
||||||
return Err(ZipError::FileReadError(format!("Error al leer archivo {}: {}", file_id, e)).into());
|
return Err(ZipError::FileReadError(format!("Error reading file {}: {}", file_id, e)).into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Escribir archivo al ZIP
|
// Write file to the ZIP
|
||||||
match zip.start_file_from_path(std::path::Path::new(&file_path), *options) {
|
match zip.start_file_from_path(std::path::Path::new(&file_path), *options) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
match zip.write_all(&content) {
|
match zip.write_all(&content) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Archivo agregado al ZIP: {}", file_path);
|
debug!("File added to ZIP: {}", file_path);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al escribir contenido del archivo {}: {}", file_path, e);
|
error!("Error writing file content {}: {}", file_path, e);
|
||||||
Err(ZipError::IoError(e).into())
|
Err(ZipError::IoError(e).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al iniciar archivo en ZIP {}: {}", file_path, e);
|
error!("Error starting file in ZIP {}: {}", file_path, e);
|
||||||
Err(ZipError::ZipError(e).into())
|
Err(ZipError::ZipError(e).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use crate::application::dtos::user_dto::{
|
|||||||
use crate::interfaces::errors::AppError;
|
use crate::interfaces::errors::AppError;
|
||||||
|
|
||||||
pub fn auth_routes() -> Router<Arc<AppState>> {
|
pub fn auth_routes() -> Router<Arc<AppState>> {
|
||||||
// Rutas que NO requieren autenticación
|
// Routes that do NOT require authentication
|
||||||
let public_routes = Router::new()
|
let public_routes = Router::new()
|
||||||
.route("/register", post(register))
|
.route("/register", post(register))
|
||||||
.route("/login", post(login))
|
.route("/login", post(login))
|
||||||
@@ -27,14 +27,14 @@ pub fn auth_routes() -> Router<Arc<AppState>> {
|
|||||||
.route("/oidc/callback", get(oidc_callback))
|
.route("/oidc/callback", get(oidc_callback))
|
||||||
.route("/oidc/exchange", post(oidc_exchange));
|
.route("/oidc/exchange", post(oidc_exchange));
|
||||||
|
|
||||||
// Rutas que SÍ requieren autenticación - usamos route_layer para aplicar middleware
|
// Routes that DO require authentication - we use route_layer to apply middleware
|
||||||
// El middleware usará el state que se pase con .with_state() desde main.rs
|
// The middleware will use the state passed with .with_state() from main.rs
|
||||||
let protected_routes = Router::new()
|
let protected_routes = Router::new()
|
||||||
.route("/me", get(get_current_user))
|
.route("/me", get(get_current_user))
|
||||||
.route("/change-password", put(change_password))
|
.route("/change-password", put(change_password))
|
||||||
.route("/logout", post(logout));
|
.route("/logout", post(logout));
|
||||||
|
|
||||||
// Combinar rutas públicas y protegidas
|
// Combine public and protected routes
|
||||||
public_routes.merge(protected_routes)
|
public_routes.merge(protected_routes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ async fn register(
|
|||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
tracing::error!("Auth service not configured");
|
tracing::error!("Auth service not configured");
|
||||||
return Err(AppError::internal_error("Servicio de autenticación no configurado"));
|
return Err(AppError::internal_error("Authentication service not configured"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ async fn login(
|
|||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
tracing::error!("Auth service not configured");
|
tracing::error!("Auth service not configured");
|
||||||
return Err(AppError::internal_error("Servicio de autenticación no configurado"));
|
return Err(AppError::internal_error("Authentication service not configured"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ async fn login(
|
|||||||
// Ensure the response has the expected fields
|
// Ensure the response has the expected fields
|
||||||
if auth_response.access_token.is_empty() || auth_response.refresh_token.is_empty() {
|
if auth_response.access_token.is_empty() || auth_response.refresh_token.is_empty() {
|
||||||
tracing::error!("Login response contains empty tokens for user: {}", dto.username);
|
tracing::error!("Login response contains empty tokens for user: {}", dto.username);
|
||||||
return Err(AppError::internal_error("Error generando tokens de autenticación"));
|
return Err(AppError::internal_error("Error generating authentication tokens"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((StatusCode::OK, Json(auth_response)))
|
Ok((StatusCode::OK, Json(auth_response)))
|
||||||
@@ -198,7 +198,7 @@ async fn refresh_token(
|
|||||||
|
|
||||||
// Normal process for real tokens
|
// Normal process for real tokens
|
||||||
let auth_service = state.auth_service.as_ref()
|
let auth_service = state.auth_service.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
||||||
|
|
||||||
let auth_response = auth_service.auth_application_service.refresh_token(dto).await?;
|
let auth_response = auth_service.auth_application_service.refresh_token(dto).await?;
|
||||||
|
|
||||||
@@ -214,37 +214,37 @@ async fn get_current_user(
|
|||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
// Normal process for all users
|
// Normal process for all users
|
||||||
let auth_service = state.auth_service.as_ref()
|
let auth_service = state.auth_service.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
||||||
|
|
||||||
// Extraer y validar el token directamente
|
// Extract and validate the token directly
|
||||||
let token = headers
|
let token = headers
|
||||||
.get(header::AUTHORIZATION)
|
.get(header::AUTHORIZATION)
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.and_then(|value| value.strip_prefix("Bearer "))
|
.and_then(|value| value.strip_prefix("Bearer "))
|
||||||
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
|
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
|
||||||
|
|
||||||
// Validar el token y obtener claims
|
// Validate the token and get claims
|
||||||
let claims = auth_service.token_service.validate_token(token)
|
let claims = auth_service.token_service.validate_token(token)
|
||||||
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
|
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
|
||||||
|
|
||||||
let user_id = claims.sub;
|
let user_id = claims.sub;
|
||||||
|
|
||||||
// Primero, actualizar las estadísticas de uso de almacenamiento
|
// First, update the storage usage statistics
|
||||||
// IMPORTANTE: Esperamos el cálculo para devolver datos actualizados
|
// IMPORTANT: We await the calculation to return updated data
|
||||||
if let Some(storage_usage_service) = state.storage_usage_service.as_ref() {
|
if let Some(storage_usage_service) = state.storage_usage_service.as_ref() {
|
||||||
// Calcular storage de forma síncrona (esperamos el resultado)
|
// Calculate storage synchronously (we await the result)
|
||||||
match storage_usage_service.update_user_storage_usage(&user_id).await {
|
match storage_usage_service.update_user_storage_usage(&user_id).await {
|
||||||
Ok(usage) => {
|
Ok(usage) => {
|
||||||
tracing::info!("Updated storage usage for user {}: {} bytes", user_id, usage);
|
tracing::info!("Updated storage usage for user {}: {} bytes", user_id, usage);
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Solo log de warning, no fallar la petición completa
|
// Only log a warning, don't fail the entire request
|
||||||
tracing::warn!("Failed to update storage usage for user {}: {}", user_id, e);
|
tracing::warn!("Failed to update storage usage for user {}: {}", user_id, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ahora obtener los datos del usuario CON el almacenamiento actualizado
|
// Now get the user data WITH the updated storage
|
||||||
let user = auth_service.auth_application_service.get_user_by_id(&user_id).await?;
|
let user = auth_service.auth_application_service.get_user_by_id(&user_id).await?;
|
||||||
|
|
||||||
Ok((StatusCode::OK, Json(user)))
|
Ok((StatusCode::OK, Json(user)))
|
||||||
@@ -256,18 +256,18 @@ async fn change_password(
|
|||||||
Json(dto): Json<ChangePasswordDto>,
|
Json(dto): Json<ChangePasswordDto>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let auth_service = state.auth_service.as_ref()
|
let auth_service = state.auth_service.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
||||||
|
|
||||||
// Extraer y validar el token directamente
|
// Extract and validate the token directly
|
||||||
let token = headers
|
let token = headers
|
||||||
.get(header::AUTHORIZATION)
|
.get(header::AUTHORIZATION)
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.and_then(|value| value.strip_prefix("Bearer "))
|
.and_then(|value| value.strip_prefix("Bearer "))
|
||||||
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
|
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
|
||||||
|
|
||||||
// Validar el token y obtener claims
|
// Validate the token and get claims
|
||||||
let claims = auth_service.token_service.validate_token(token)
|
let claims = auth_service.token_service.validate_token(token)
|
||||||
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
|
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
|
||||||
|
|
||||||
auth_service.auth_application_service.change_password(&claims.sub, dto).await?;
|
auth_service.auth_application_service.change_password(&claims.sub, dto).await?;
|
||||||
|
|
||||||
@@ -279,18 +279,18 @@ async fn logout(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let auth_service = state.auth_service.as_ref()
|
let auth_service = state.auth_service.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
||||||
|
|
||||||
// Extraer y validar el token directamente
|
// Extract and validate the token directly
|
||||||
let token = headers
|
let token = headers
|
||||||
.get(header::AUTHORIZATION)
|
.get(header::AUTHORIZATION)
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.and_then(|value| value.strip_prefix("Bearer "))
|
.and_then(|value| value.strip_prefix("Bearer "))
|
||||||
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
|
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
|
||||||
|
|
||||||
// Validar el token y obtener claims
|
// Validate the token and get claims
|
||||||
let claims = auth_service.token_service.validate_token(token)
|
let claims = auth_service.token_service.validate_token(token)
|
||||||
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
|
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
|
||||||
|
|
||||||
// Use access token for logout (we don't have refresh token in headers)
|
// Use access token for logout (we don't have refresh token in headers)
|
||||||
auth_service.auth_application_service.logout(&claims.sub, token).await?;
|
auth_service.auth_application_service.logout(&claims.sub, token).await?;
|
||||||
@@ -314,7 +314,7 @@ async fn get_system_status(
|
|||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let auth_service = state.auth_service.as_ref()
|
let auth_service = state.auth_service.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
||||||
|
|
||||||
// Count admin users to determine if system is initialized
|
// Count admin users to determine if system is initialized
|
||||||
let admin_count = auth_service.auth_application_service.count_admin_users().await
|
let admin_count = auth_service.auth_application_service.count_admin_users().await
|
||||||
|
|||||||
@@ -13,86 +13,86 @@ use crate::application::dtos::file_dto::FileDto;
|
|||||||
use crate::application::dtos::folder_dto::FolderDto;
|
use crate::application::dtos::folder_dto::FolderDto;
|
||||||
use crate::interfaces::api::handlers::ApiResult;
|
use crate::interfaces::api::handlers::ApiResult;
|
||||||
|
|
||||||
/// Estado compartido para el handler de batch
|
/// Shared state for the batch handler
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BatchHandlerState {
|
pub struct BatchHandlerState {
|
||||||
pub batch_service: Arc<BatchOperationService>,
|
pub batch_service: Arc<BatchOperationService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DTO para las solicitudes de operaciones en lote de archivos
|
/// DTO for batch file operation requests
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct BatchFileOperationRequest {
|
pub struct BatchFileOperationRequest {
|
||||||
/// IDs de los archivos a procesar
|
/// IDs of the files to process
|
||||||
pub file_ids: Vec<String>,
|
pub file_ids: Vec<String>,
|
||||||
/// ID de la carpeta destino (opcional)
|
/// Target folder ID (optional)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub target_folder_id: Option<String>,
|
pub target_folder_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DTO para las solicitudes de operaciones en lote de carpetas
|
/// DTO for batch folder operation requests
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct BatchFolderOperationRequest {
|
pub struct BatchFolderOperationRequest {
|
||||||
/// IDs de las carpetas a procesar
|
/// IDs of the folders to process
|
||||||
pub folder_ids: Vec<String>,
|
pub folder_ids: Vec<String>,
|
||||||
/// Si la operación debe ser recursiva
|
/// Whether the operation should be recursive
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub recursive: bool,
|
pub recursive: bool,
|
||||||
/// ID de la carpeta destino (opcional)
|
/// Target folder ID (optional)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub target_folder_id: Option<String>,
|
pub target_folder_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DTO para las solicitudes de creación en lote de carpetas
|
/// DTO for batch folder creation requests
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct BatchCreateFoldersRequest {
|
pub struct BatchCreateFoldersRequest {
|
||||||
/// Detalles de las carpetas a crear
|
/// Details of the folders to create
|
||||||
pub folders: Vec<CreateFolderDetail>,
|
pub folders: Vec<CreateFolderDetail>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detalle para creación de una carpeta
|
/// Detail for folder creation
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct CreateFolderDetail {
|
pub struct CreateFolderDetail {
|
||||||
/// Nombre de la carpeta
|
/// Folder name
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// ID de la carpeta padre (opcional)
|
/// Parent folder ID (optional)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub parent_id: Option<String>,
|
pub parent_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DTO para los resultados de operaciones en lote
|
/// DTO for batch operation results
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct BatchOperationResponse<T> {
|
pub struct BatchOperationResponse<T> {
|
||||||
/// Entidades procesadas exitosamente
|
/// Successfully processed entities
|
||||||
pub successful: Vec<T>,
|
pub successful: Vec<T>,
|
||||||
/// Operaciones fallidas con sus mensajes de error
|
/// Failed operations with their error messages
|
||||||
pub failed: Vec<FailedOperation>,
|
pub failed: Vec<FailedOperation>,
|
||||||
/// Estadísticas de la operación
|
/// Operation statistics
|
||||||
pub stats: BatchOperationStats,
|
pub stats: BatchOperationStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Operación fallida en un lote
|
/// Failed operation in a batch
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct FailedOperation {
|
pub struct FailedOperation {
|
||||||
/// Identificador de la entidad que falló
|
/// Identifier of the entity that failed
|
||||||
pub id: String,
|
pub id: String,
|
||||||
/// Mensaje de error
|
/// Error message
|
||||||
pub error: String,
|
pub error: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Estadísticas de una operación por lotes
|
/// Statistics for a batch operation
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct BatchOperationStats {
|
pub struct BatchOperationStats {
|
||||||
/// Número total de operaciones
|
/// Total number of operations
|
||||||
pub total: usize,
|
pub total: usize,
|
||||||
/// Número de operaciones exitosas
|
/// Number of successful operations
|
||||||
pub successful: usize,
|
pub successful: usize,
|
||||||
/// Número de operaciones fallidas
|
/// Number of failed operations
|
||||||
pub failed: usize,
|
pub failed: usize,
|
||||||
/// Tiempo total de ejecución en milisegundos
|
/// Total execution time in milliseconds
|
||||||
pub execution_time_ms: u128,
|
pub execution_time_ms: u128,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convierte BatchStats del dominio a DTO
|
/// Converts domain BatchStats to DTO
|
||||||
impl From<BatchStats> for BatchOperationStats {
|
impl From<BatchStats> for BatchOperationStats {
|
||||||
fn from(stats: BatchStats) -> Self {
|
fn from(stats: BatchStats) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -104,7 +104,7 @@ impl From<BatchStats> for BatchOperationStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convierte BatchResult<T> del dominio a DTO
|
/// Converts domain BatchResult<T> to DTO
|
||||||
impl<T, U> From<BatchResult<T>> for BatchOperationResponse<U>
|
impl<T, U> From<BatchResult<T>> for BatchOperationResponse<U>
|
||||||
where
|
where
|
||||||
U: From<T>,
|
U: From<T>,
|
||||||
@@ -124,12 +124,12 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler para mover múltiples archivos en lote
|
/// Handler for moving multiple files in batch
|
||||||
pub async fn move_files_batch(
|
pub async fn move_files_batch(
|
||||||
State(state): State<BatchHandlerState>,
|
State(state): State<BatchHandlerState>,
|
||||||
Json(request): Json<BatchFileOperationRequest>,
|
Json(request): Json<BatchFileOperationRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// Verificar que hay archivos para procesar
|
// Verify there are files to process
|
||||||
if request.file_ids.is_empty() {
|
if request.file_ids.is_empty() {
|
||||||
return Ok((
|
return Ok((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
@@ -139,35 +139,35 @@ pub async fn move_files_batch(
|
|||||||
).into_response());
|
).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecutar operación de lote
|
// Execute batch operation
|
||||||
let result = state.batch_service
|
let result = state.batch_service
|
||||||
.move_files(request.file_ids, request.target_folder_id)
|
.move_files(request.file_ids, request.target_folder_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Convertir resultado a DTO
|
// Convert result to DTO
|
||||||
let response: BatchOperationResponse<FileDto> = result.into();
|
let response: BatchOperationResponse<FileDto> = result.into();
|
||||||
|
|
||||||
// Determinar código de estado basado en los resultados
|
// Determine status code based on results
|
||||||
let status_code = if response.stats.failed > 0 {
|
let status_code = if response.stats.failed > 0 {
|
||||||
if response.stats.successful > 0 {
|
if response.stats.successful > 0 {
|
||||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||||
} else {
|
} else {
|
||||||
StatusCode::BAD_REQUEST // Todas fallaron
|
StatusCode::BAD_REQUEST // All failed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StatusCode::OK // Todas exitosas
|
StatusCode::OK // All successful
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((status_code, Json(response)).into_response())
|
Ok((status_code, Json(response)).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler para copiar múltiples archivos en lote
|
/// Handler for copying multiple files in batch
|
||||||
pub async fn copy_files_batch(
|
pub async fn copy_files_batch(
|
||||||
State(state): State<BatchHandlerState>,
|
State(state): State<BatchHandlerState>,
|
||||||
Json(request): Json<BatchFileOperationRequest>,
|
Json(request): Json<BatchFileOperationRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// Verificar que hay archivos para procesar
|
// Verify there are files to process
|
||||||
if request.file_ids.is_empty() {
|
if request.file_ids.is_empty() {
|
||||||
return Ok((
|
return Ok((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
@@ -177,35 +177,35 @@ pub async fn copy_files_batch(
|
|||||||
).into_response());
|
).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecutar operación de lote
|
// Execute batch operation
|
||||||
let result = state.batch_service
|
let result = state.batch_service
|
||||||
.copy_files(request.file_ids, request.target_folder_id)
|
.copy_files(request.file_ids, request.target_folder_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Convertir resultado a DTO
|
// Convert result to DTO
|
||||||
let response: BatchOperationResponse<FileDto> = result.into();
|
let response: BatchOperationResponse<FileDto> = result.into();
|
||||||
|
|
||||||
// Determinar código de estado basado en los resultados
|
// Determine status code based on results
|
||||||
let status_code = if response.stats.failed > 0 {
|
let status_code = if response.stats.failed > 0 {
|
||||||
if response.stats.successful > 0 {
|
if response.stats.successful > 0 {
|
||||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||||
} else {
|
} else {
|
||||||
StatusCode::BAD_REQUEST // Todas fallaron
|
StatusCode::BAD_REQUEST // All failed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StatusCode::OK // Todas exitosas
|
StatusCode::OK // All successful
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((status_code, Json(response)).into_response())
|
Ok((status_code, Json(response)).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler para eliminar múltiples archivos en lote
|
/// Handler for deleting multiple files in batch
|
||||||
pub async fn delete_files_batch(
|
pub async fn delete_files_batch(
|
||||||
State(state): State<BatchHandlerState>,
|
State(state): State<BatchHandlerState>,
|
||||||
Json(request): Json<BatchFileOperationRequest>,
|
Json(request): Json<BatchFileOperationRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// Verificar que hay archivos para procesar
|
// Verify there are files to process
|
||||||
if request.file_ids.is_empty() {
|
if request.file_ids.is_empty() {
|
||||||
return Ok((
|
return Ok((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
@@ -215,13 +215,13 @@ pub async fn delete_files_batch(
|
|||||||
).into_response());
|
).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecutar operación de lote
|
// Execute batch operation
|
||||||
let result = state.batch_service
|
let result = state.batch_service
|
||||||
.delete_files(request.file_ids)
|
.delete_files(request.file_ids)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Crear respuesta personalizada para IDs de string
|
// Create custom response for string IDs
|
||||||
let response = BatchOperationResponse {
|
let response = BatchOperationResponse {
|
||||||
successful: result.successful,
|
successful: result.successful,
|
||||||
failed: result.failed.into_iter()
|
failed: result.failed.into_iter()
|
||||||
@@ -230,26 +230,26 @@ pub async fn delete_files_batch(
|
|||||||
stats: result.stats.into(),
|
stats: result.stats.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determinar código de estado basado en los resultados
|
// Determine status code based on results
|
||||||
let status_code = if response.stats.failed > 0 {
|
let status_code = if response.stats.failed > 0 {
|
||||||
if response.stats.successful > 0 {
|
if response.stats.successful > 0 {
|
||||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||||
} else {
|
} else {
|
||||||
StatusCode::BAD_REQUEST // Todas fallaron
|
StatusCode::BAD_REQUEST // All failed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StatusCode::OK // Todas exitosas
|
StatusCode::OK // All successful
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((status_code, Json(response)).into_response())
|
Ok((status_code, Json(response)).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler para eliminar múltiples carpetas en lote
|
/// Handler for deleting multiple folders in batch
|
||||||
pub async fn delete_folders_batch(
|
pub async fn delete_folders_batch(
|
||||||
State(state): State<BatchHandlerState>,
|
State(state): State<BatchHandlerState>,
|
||||||
Json(request): Json<BatchFolderOperationRequest>,
|
Json(request): Json<BatchFolderOperationRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// Verificar que hay carpetas para procesar
|
// Verify there are folders to process
|
||||||
if request.folder_ids.is_empty() {
|
if request.folder_ids.is_empty() {
|
||||||
return Ok((
|
return Ok((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
@@ -259,13 +259,13 @@ pub async fn delete_folders_batch(
|
|||||||
).into_response());
|
).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecutar operación de lote
|
// Execute batch operation
|
||||||
let result = state.batch_service
|
let result = state.batch_service
|
||||||
.delete_folders(request.folder_ids, request.recursive)
|
.delete_folders(request.folder_ids, request.recursive)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Crear respuesta personalizada para IDs de string
|
// Create custom response for string IDs
|
||||||
let response = BatchOperationResponse {
|
let response = BatchOperationResponse {
|
||||||
successful: result.successful,
|
successful: result.successful,
|
||||||
failed: result.failed.into_iter()
|
failed: result.failed.into_iter()
|
||||||
@@ -274,26 +274,26 @@ pub async fn delete_folders_batch(
|
|||||||
stats: result.stats.into(),
|
stats: result.stats.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determinar código de estado basado en los resultados
|
// Determine status code based on results
|
||||||
let status_code = if response.stats.failed > 0 {
|
let status_code = if response.stats.failed > 0 {
|
||||||
if response.stats.successful > 0 {
|
if response.stats.successful > 0 {
|
||||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||||
} else {
|
} else {
|
||||||
StatusCode::BAD_REQUEST // Todas fallaron
|
StatusCode::BAD_REQUEST // All failed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StatusCode::OK // Todas exitosas
|
StatusCode::OK // All successful
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((status_code, Json(response)).into_response())
|
Ok((status_code, Json(response)).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler para crear múltiples carpetas en lote
|
/// Handler for creating multiple folders in batch
|
||||||
pub async fn create_folders_batch(
|
pub async fn create_folders_batch(
|
||||||
State(state): State<BatchHandlerState>,
|
State(state): State<BatchHandlerState>,
|
||||||
Json(request): Json<BatchCreateFoldersRequest>,
|
Json(request): Json<BatchCreateFoldersRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// Verificar que hay carpetas para procesar
|
// Verify there are folders to process
|
||||||
if request.folders.is_empty() {
|
if request.folders.is_empty() {
|
||||||
return Ok((
|
return Ok((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
@@ -303,41 +303,41 @@ pub async fn create_folders_batch(
|
|||||||
).into_response());
|
).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transformar el formato para el servicio
|
// Transform the format for the service
|
||||||
let folders = request.folders
|
let folders = request.folders
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|detail| (detail.name, detail.parent_id))
|
.map(|detail| (detail.name, detail.parent_id))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Ejecutar operación de lote
|
// Execute batch operation
|
||||||
let result = state.batch_service
|
let result = state.batch_service
|
||||||
.create_folders(folders)
|
.create_folders(folders)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Convertir resultado a DTO
|
// Convert result to DTO
|
||||||
let response: BatchOperationResponse<FolderDto> = result.into();
|
let response: BatchOperationResponse<FolderDto> = result.into();
|
||||||
|
|
||||||
// Determinar código de estado basado en los resultados
|
// Determine status code based on results
|
||||||
let status_code = if response.stats.failed > 0 {
|
let status_code = if response.stats.failed > 0 {
|
||||||
if response.stats.successful > 0 {
|
if response.stats.successful > 0 {
|
||||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||||
} else {
|
} else {
|
||||||
StatusCode::BAD_REQUEST // Todas fallaron
|
StatusCode::BAD_REQUEST // All failed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StatusCode::CREATED // Todas exitosas
|
StatusCode::CREATED // All successful
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((status_code, Json(response)).into_response())
|
Ok((status_code, Json(response)).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler para obtener múltiples archivos en lote
|
/// Handler for getting multiple files in batch
|
||||||
pub async fn get_files_batch(
|
pub async fn get_files_batch(
|
||||||
State(state): State<BatchHandlerState>,
|
State(state): State<BatchHandlerState>,
|
||||||
Json(request): Json<BatchFileOperationRequest>,
|
Json(request): Json<BatchFileOperationRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// Verificar que hay archivos para procesar
|
// Verify there are files to process
|
||||||
if request.file_ids.is_empty() {
|
if request.file_ids.is_empty() {
|
||||||
return Ok((
|
return Ok((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
@@ -347,35 +347,35 @@ pub async fn get_files_batch(
|
|||||||
).into_response());
|
).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecutar operación de lote
|
// Execute batch operation
|
||||||
let result = state.batch_service
|
let result = state.batch_service
|
||||||
.get_multiple_files(request.file_ids)
|
.get_multiple_files(request.file_ids)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Convertir resultado a DTO
|
// Convert result to DTO
|
||||||
let response: BatchOperationResponse<FileDto> = result.into();
|
let response: BatchOperationResponse<FileDto> = result.into();
|
||||||
|
|
||||||
// Determinar código de estado basado en los resultados
|
// Determine status code based on results
|
||||||
let status_code = if response.stats.failed > 0 {
|
let status_code = if response.stats.failed > 0 {
|
||||||
if response.stats.successful > 0 {
|
if response.stats.successful > 0 {
|
||||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||||
} else {
|
} else {
|
||||||
StatusCode::BAD_REQUEST // Todas fallaron
|
StatusCode::BAD_REQUEST // All failed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StatusCode::OK // Todas exitosas
|
StatusCode::OK // All successful
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((status_code, Json(response)).into_response())
|
Ok((status_code, Json(response)).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler para obtener múltiples carpetas en lote
|
/// Handler for getting multiple folders in batch
|
||||||
pub async fn get_folders_batch(
|
pub async fn get_folders_batch(
|
||||||
State(state): State<BatchHandlerState>,
|
State(state): State<BatchHandlerState>,
|
||||||
Json(request): Json<BatchFolderOperationRequest>,
|
Json(request): Json<BatchFolderOperationRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// Verificar que hay carpetas para procesar
|
// Verify there are folders to process
|
||||||
if request.folder_ids.is_empty() {
|
if request.folder_ids.is_empty() {
|
||||||
return Ok((
|
return Ok((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
@@ -385,24 +385,24 @@ pub async fn get_folders_batch(
|
|||||||
).into_response());
|
).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecutar operación de lote
|
// Execute batch operation
|
||||||
let result = state.batch_service
|
let result = state.batch_service
|
||||||
.get_multiple_folders(request.folder_ids)
|
.get_multiple_folders(request.folder_ids)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Convertir resultado a DTO
|
// Convert result to DTO
|
||||||
let response: BatchOperationResponse<FolderDto> = result.into();
|
let response: BatchOperationResponse<FolderDto> = result.into();
|
||||||
|
|
||||||
// Determinar código de estado basado en los resultados
|
// Determine status code based on results
|
||||||
let status_code = if response.stats.failed > 0 {
|
let status_code = if response.stats.failed > 0 {
|
||||||
if response.stats.successful > 0 {
|
if response.stats.successful > 0 {
|
||||||
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
||||||
} else {
|
} else {
|
||||||
StatusCode::BAD_REQUEST // Todas fallaron
|
StatusCode::BAD_REQUEST // All failed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StatusCode::OK // Todas exitosas
|
StatusCode::OK // All successful
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((status_code, Json(response)).into_response())
|
Ok((status_code, Json(response)).into_response())
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ use tracing::{error, info};
|
|||||||
use crate::application::ports::recent_ports::RecentItemsUseCase;
|
use crate::application::ports::recent_ports::RecentItemsUseCase;
|
||||||
use crate::interfaces::middleware::auth::AuthUser;
|
use crate::interfaces::middleware::auth::AuthUser;
|
||||||
|
|
||||||
/// Parámetros de consulta para obtener elementos recientes
|
/// Query parameters for getting recent items
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct GetRecentParams {
|
pub struct GetRecentParams {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
limit: Option<i32>,
|
limit: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtener elementos recientes del usuario
|
/// Get user's recent items
|
||||||
pub async fn get_recent_items(
|
pub async fn get_recent_items(
|
||||||
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
@@ -28,22 +28,22 @@ pub async fn get_recent_items(
|
|||||||
|
|
||||||
match recent_service.get_recent_items(user_id, params.limit).await {
|
match recent_service.get_recent_items(user_id, params.limit).await {
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
info!("Recuperados {} elementos recientes para usuario", items.len());
|
info!("Retrieved {} recent items for user", items.len());
|
||||||
(StatusCode::OK, Json(items)).into_response()
|
(StatusCode::OK, Json(items)).into_response()
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error al recuperar elementos recientes: {}", err);
|
error!("Error retrieving recent items: {}", err);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"error": format!("Fallo al recuperar elementos recientes: {}", err)
|
"error": format!("Failed to retrieve recent items: {}", err)
|
||||||
}))
|
}))
|
||||||
).into_response()
|
).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registrar acceso a un elemento
|
/// Record access to an item
|
||||||
pub async fn record_item_access(
|
pub async fn record_item_access(
|
||||||
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
@@ -51,39 +51,39 @@ pub async fn record_item_access(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &auth_user.id;
|
let user_id = &auth_user.id;
|
||||||
|
|
||||||
// Validar tipo de elemento
|
// Validate item type
|
||||||
if item_type != "file" && item_type != "folder" {
|
if item_type != "file" && item_type != "folder" {
|
||||||
return (
|
return (
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"error": "El tipo de elemento debe ser 'file' o 'folder'"
|
"error": "Item type must be 'file' or 'folder'"
|
||||||
}))
|
}))
|
||||||
).into_response();
|
).into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
match recent_service.record_item_access(user_id, &item_id, &item_type).await {
|
match recent_service.record_item_access(user_id, &item_id, &item_type).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!("Registrado acceso a {} '{}' en recientes", item_type, item_id);
|
info!("Recorded access to {} '{}' in recents", item_type, item_id);
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"message": "Acceso registrado correctamente"
|
"message": "Access recorded successfully"
|
||||||
}))
|
}))
|
||||||
).into_response()
|
).into_response()
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error al registrar acceso en recientes: {}", err);
|
error!("Error recording access in recents: {}", err);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"error": format!("Fallo al registrar acceso: {}", err)
|
"error": format!("Failed to record access: {}", err)
|
||||||
}))
|
}))
|
||||||
).into_response()
|
).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Eliminar un elemento de recientes
|
/// Remove an item from recents
|
||||||
pub async fn remove_from_recent(
|
pub async fn remove_from_recent(
|
||||||
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
@@ -94,36 +94,36 @@ pub async fn remove_from_recent(
|
|||||||
match recent_service.remove_from_recent(user_id, &item_id, &item_type).await {
|
match recent_service.remove_from_recent(user_id, &item_id, &item_type).await {
|
||||||
Ok(removed) => {
|
Ok(removed) => {
|
||||||
if removed {
|
if removed {
|
||||||
info!("Eliminado {} '{}' de recientes", item_type, item_id);
|
info!("Removed {} '{}' from recents", item_type, item_id);
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"message": "Elemento eliminado de recientes"
|
"message": "Item removed from recents"
|
||||||
}))
|
}))
|
||||||
).into_response()
|
).into_response()
|
||||||
} else {
|
} else {
|
||||||
info!("Elemento {} '{}' no estaba en recientes", item_type, item_id);
|
info!("Item {} '{}' was not in recents", item_type, item_id);
|
||||||
(
|
(
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"message": "Elemento no estaba en recientes"
|
"message": "Item was not in recents"
|
||||||
}))
|
}))
|
||||||
).into_response()
|
).into_response()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error al eliminar de recientes: {}", err);
|
error!("Error removing from recents: {}", err);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"error": format!("Fallo al eliminar de recientes: {}", err)
|
"error": format!("Failed to remove from recents: {}", err)
|
||||||
}))
|
}))
|
||||||
).into_response()
|
).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Limpiar todos los elementos recientes
|
/// Clear all recent items
|
||||||
pub async fn clear_recent_items(
|
pub async fn clear_recent_items(
|
||||||
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
@@ -132,20 +132,20 @@ pub async fn clear_recent_items(
|
|||||||
|
|
||||||
match recent_service.clear_recent_items(user_id).await {
|
match recent_service.clear_recent_items(user_id).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!("Limpiados todos los elementos recientes para usuario");
|
info!("Cleared all recent items for user");
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"message": "Elementos recientes limpiados correctamente"
|
"message": "Recent items cleared successfully"
|
||||||
}))
|
}))
|
||||||
).into_response()
|
).into_response()
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error al limpiar elementos recientes: {}", err);
|
error!("Error clearing recent items: {}", err);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"error": format!("Fallo al limpiar elementos recientes: {}", err)
|
"error": format!("Failed to clear recent items: {}", err)
|
||||||
}))
|
}))
|
||||||
).into_response()
|
).into_response()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,34 +10,34 @@ use crate::application::dtos::search_dto::SearchCriteriaDto;
|
|||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manejador para las operaciones de búsqueda a través de la API.
|
* Handler for search operations through the API.
|
||||||
*
|
*
|
||||||
* Este manejador expone endpoints relacionados con la funcionalidad de búsqueda,
|
* This handler exposes endpoints related to search functionality,
|
||||||
* permitiendo a los usuarios buscar archivos y carpetas usando diversos criterios.
|
* allowing users to search for files and folders using various criteria.
|
||||||
*/
|
*/
|
||||||
pub struct SearchHandler;
|
pub struct SearchHandler;
|
||||||
|
|
||||||
impl SearchHandler {
|
impl SearchHandler {
|
||||||
/**
|
/**
|
||||||
* Realiza una búsqueda basada en los criterios proporcionados como parámetros de consulta.
|
* Performs a search based on the criteria provided as query parameters.
|
||||||
*
|
*
|
||||||
* Este endpoint permite búsquedas simples directamente con parámetros URL.
|
* This endpoint allows simple searches directly with URL parameters.
|
||||||
*
|
*
|
||||||
* @param state Estado de la aplicación con servicios
|
* @param state Application state with services
|
||||||
* @param query_params Parámetros de búsqueda como query string
|
* @param query_params Search parameters as query string
|
||||||
* @return Respuesta HTTP con los resultados de la búsqueda
|
* @return HTTP response with the search results
|
||||||
*/
|
*/
|
||||||
pub async fn search_files_get(
|
pub async fn search_files_get(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(params): Query<SearchParams>,
|
Query(params): Query<SearchParams>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
info!("API: Búsqueda de archivos con parámetros: {:?}", params);
|
info!("API: File search with parameters: {:?}", params);
|
||||||
|
|
||||||
// Extraer el servicio de búsqueda o devolver error si no está disponible
|
// Extract the search service or return error if not available
|
||||||
let search_service = match &state.applications.search_service {
|
let search_service = match &state.applications.search_service {
|
||||||
Some(service) => service,
|
Some(service) => service,
|
||||||
None => {
|
None => {
|
||||||
error!("Servicio de búsqueda no disponible");
|
error!("Search service not available");
|
||||||
return (
|
return (
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
Json(json!({
|
Json(json!({
|
||||||
@@ -47,7 +47,7 @@ impl SearchHandler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convertir parámetros de búsqueda a DTO
|
// Convert search parameters to DTO
|
||||||
let search_criteria = SearchCriteriaDto {
|
let search_criteria = SearchCriteriaDto {
|
||||||
name_contains: params.query,
|
name_contains: params.query,
|
||||||
file_types: params.type_filter.map(|t| t.split(',').map(|s| s.trim().to_string()).collect()),
|
file_types: params.type_filter.map(|t| t.split(',').map(|s| s.trim().to_string()).collect()),
|
||||||
@@ -63,15 +63,15 @@ impl SearchHandler {
|
|||||||
offset: params.offset.unwrap_or(0),
|
offset: params.offset.unwrap_or(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Realizar la búsqueda
|
// Perform the search
|
||||||
match search_service.search(search_criteria).await {
|
match search_service.search(search_criteria).await {
|
||||||
Ok(results) => {
|
Ok(results) => {
|
||||||
info!("Búsqueda completada, {} archivos y {} carpetas encontrados",
|
info!("Search completed, {} files and {} folders found",
|
||||||
results.files.len(), results.folders.len());
|
results.files.len(), results.folders.len());
|
||||||
(StatusCode::OK, Json(results)).into_response()
|
(StatusCode::OK, Json(results)).into_response()
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error en búsqueda: {}", err);
|
error!("Search error: {}", err);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({
|
Json(json!({
|
||||||
@@ -83,26 +83,26 @@ impl SearchHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Realiza una búsqueda avanzada basada en un objeto de criterios JSON completo.
|
* Performs an advanced search based on a complete JSON criteria object.
|
||||||
*
|
*
|
||||||
* Este endpoint permite búsquedas más complejas con todos los criterios posibles
|
* This endpoint allows more complex searches with all possible criteria
|
||||||
* proporcionados en el cuerpo de la solicitud.
|
* provided in the request body.
|
||||||
*
|
*
|
||||||
* @param state Estado de la aplicación con servicios
|
* @param state Application state with services
|
||||||
* @param criteria Criterios de búsqueda completos
|
* @param criteria Complete search criteria
|
||||||
* @return Respuesta HTTP con los resultados de la búsqueda
|
* @return HTTP response with the search results
|
||||||
*/
|
*/
|
||||||
pub async fn search_files_post(
|
pub async fn search_files_post(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(criteria): Json<SearchCriteriaDto>,
|
Json(criteria): Json<SearchCriteriaDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
info!("API: Búsqueda avanzada de archivos");
|
info!("API: Advanced file search");
|
||||||
|
|
||||||
// Extraer el servicio de búsqueda o devolver error si no está disponible
|
// Extract the search service or return error if not available
|
||||||
let search_service = match &state.applications.search_service {
|
let search_service = match &state.applications.search_service {
|
||||||
Some(service) => service,
|
Some(service) => service,
|
||||||
None => {
|
None => {
|
||||||
error!("Servicio de búsqueda no disponible");
|
error!("Search service not available");
|
||||||
return (
|
return (
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
Json(json!({
|
Json(json!({
|
||||||
@@ -112,15 +112,15 @@ impl SearchHandler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Realizar la búsqueda
|
// Perform the search
|
||||||
match search_service.search(criteria).await {
|
match search_service.search(criteria).await {
|
||||||
Ok(results) => {
|
Ok(results) => {
|
||||||
info!("Búsqueda completada, {} archivos y {} carpetas encontrados",
|
info!("Search completed, {} files and {} folders found",
|
||||||
results.files.len(), results.folders.len());
|
results.files.len(), results.folders.len());
|
||||||
(StatusCode::OK, Json(results)).into_response()
|
(StatusCode::OK, Json(results)).into_response()
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error en búsqueda: {}", err);
|
error!("Search error: {}", err);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({
|
Json(json!({
|
||||||
@@ -132,24 +132,24 @@ impl SearchHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limpia la caché de resultados de búsqueda.
|
* Clears the search results cache.
|
||||||
*
|
*
|
||||||
* Este endpoint es útil para forzar búsquedas frescas después de cambios
|
* This endpoint is useful for forcing fresh searches after significant
|
||||||
* significativos en el sistema de archivos.
|
* changes in the file system.
|
||||||
*
|
*
|
||||||
* @param state Estado de la aplicación con servicios
|
* @param state Application state with services
|
||||||
* @return Respuesta HTTP indicando éxito o error
|
* @return HTTP response indicating success or error
|
||||||
*/
|
*/
|
||||||
pub async fn clear_search_cache(
|
pub async fn clear_search_cache(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
info!("API: Limpiando caché de búsqueda");
|
info!("API: Clearing search cache");
|
||||||
|
|
||||||
// Extraer el servicio de búsqueda o devolver error si no está disponible
|
// Extract the search service or return error if not available
|
||||||
let search_service = match &state.applications.search_service {
|
let search_service = match &state.applications.search_service {
|
||||||
Some(service) => service,
|
Some(service) => service,
|
||||||
None => {
|
None => {
|
||||||
error!("Servicio de búsqueda no disponible");
|
error!("Search service not available");
|
||||||
return (
|
return (
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
Json(json!({
|
Json(json!({
|
||||||
@@ -159,10 +159,10 @@ impl SearchHandler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Limpiar la caché
|
// Clear the cache
|
||||||
match search_service.clear_search_cache().await {
|
match search_service.clear_search_cache().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!("Caché de búsqueda limpiada correctamente");
|
info!("Search cache cleared successfully");
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(json!({
|
Json(json!({
|
||||||
@@ -171,7 +171,7 @@ impl SearchHandler {
|
|||||||
).into_response()
|
).into_response()
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error al limpiar caché de búsqueda: {}", err);
|
error!("Error clearing search cache: {}", err);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({
|
Json(json!({
|
||||||
@@ -183,43 +183,43 @@ impl SearchHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parámetros de búsqueda para el endpoint GET
|
/// Search parameters for the GET endpoint
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct SearchParams {
|
pub struct SearchParams {
|
||||||
/// Texto a buscar en nombres de archivos y carpetas
|
/// Text to search for in file and folder names
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
|
|
||||||
/// Filtro por tipos de archivo (extensiones separadas por comas)
|
/// Filter by file types (comma-separated extensions)
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub type_filter: Option<String>,
|
pub type_filter: Option<String>,
|
||||||
|
|
||||||
/// Filtrar elementos creados después de esta fecha (timestamp)
|
/// Filter items created after this date (timestamp)
|
||||||
pub created_after: Option<u64>,
|
pub created_after: Option<u64>,
|
||||||
|
|
||||||
/// Filtrar elementos creados antes de esta fecha (timestamp)
|
/// Filter items created before this date (timestamp)
|
||||||
pub created_before: Option<u64>,
|
pub created_before: Option<u64>,
|
||||||
|
|
||||||
/// Filtrar elementos modificados después de esta fecha (timestamp)
|
/// Filter items modified after this date (timestamp)
|
||||||
pub modified_after: Option<u64>,
|
pub modified_after: Option<u64>,
|
||||||
|
|
||||||
/// Filtrar elementos modificados antes de esta fecha (timestamp)
|
/// Filter items modified before this date (timestamp)
|
||||||
pub modified_before: Option<u64>,
|
pub modified_before: Option<u64>,
|
||||||
|
|
||||||
/// Tamaño mínimo en bytes
|
/// Minimum size in bytes
|
||||||
pub min_size: Option<u64>,
|
pub min_size: Option<u64>,
|
||||||
|
|
||||||
/// Tamaño máximo en bytes
|
/// Maximum size in bytes
|
||||||
pub max_size: Option<u64>,
|
pub max_size: Option<u64>,
|
||||||
|
|
||||||
/// ID de carpeta para limitar la búsqueda
|
/// Folder ID to limit the search scope
|
||||||
pub folder_id: Option<String>,
|
pub folder_id: Option<String>,
|
||||||
|
|
||||||
/// Búsqueda recursiva en subcarpetas
|
/// Recursive search in subfolders
|
||||||
pub recursive: Option<bool>,
|
pub recursive: Option<bool>,
|
||||||
|
|
||||||
/// Límite de resultados para paginación
|
/// Result limit for pagination
|
||||||
pub limit: Option<usize>,
|
pub limit: Option<usize>,
|
||||||
|
|
||||||
/// Desplazamiento para paginación
|
/// Offset for pagination
|
||||||
pub offset: Option<usize>,
|
pub offset: Option<usize>,
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ use tracing::{debug, error, warn, instrument};
|
|||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
use crate::interfaces::middleware::auth::AuthUser;
|
use crate::interfaces::middleware::auth::AuthUser;
|
||||||
|
|
||||||
/// Obtiene todos los elementos en la papelera para el usuario actual
|
/// Gets all items in the trash for the current user
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn get_trash_items(
|
pub async fn get_trash_items(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -19,7 +19,7 @@ pub async fn get_trash_items(
|
|||||||
// privilege escalation attacks.
|
// privilege escalation attacks.
|
||||||
let effective_user = auth_user.id.clone();
|
let effective_user = auth_user.id.clone();
|
||||||
|
|
||||||
debug!("Solicitud para listar elementos en papelera para usuario {}", effective_user);
|
debug!("Request to list trash items for user {}", effective_user);
|
||||||
|
|
||||||
let trash_service = match state.trash_service.as_ref() {
|
let trash_service = match state.trash_service.as_ref() {
|
||||||
Some(service) => service,
|
Some(service) => service,
|
||||||
@@ -34,11 +34,11 @@ pub async fn get_trash_items(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
debug!("Encontrados {} elementos en la papelera", items.len());
|
debug!("Found {} items in trash", items.len());
|
||||||
(StatusCode::OK, Json(json!(items)))
|
(StatusCode::OK, Json(json!(items)))
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al obtener elementos de la papelera: {:?}", e);
|
error!("Error retrieving trash items: {:?}", e);
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||||
"error": format!("Error retrieving trash items: {}", e)
|
"error": format!("Error retrieving trash items: {}", e)
|
||||||
})))
|
})))
|
||||||
@@ -46,14 +46,14 @@ pub async fn get_trash_items(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mueve un elemento (archivo o carpeta) a la papelera (función genérica, no usada directamente en rutas)
|
/// Moves an item (file or folder) to the trash (generic function, not used directly in routes)
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn move_to_trash(
|
pub async fn move_to_trash(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path((item_type, item_id)): Path<(String, String)>,
|
Path((item_type, item_id)): Path<(String, String)>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
debug!("Solicitud para mover a papelera: tipo={}, id={}, usuario={}",
|
debug!("Request to move to trash: type={}, id={}, user={}",
|
||||||
item_type, item_id, auth_user.id);
|
item_type, item_id, auth_user.id);
|
||||||
|
|
||||||
let trash_service = match state.trash_service.as_ref() {
|
let trash_service = match state.trash_service.as_ref() {
|
||||||
@@ -68,14 +68,14 @@ pub async fn move_to_trash(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Elemento movido a papelera con éxito");
|
debug!("Item moved to trash successfully");
|
||||||
(StatusCode::OK, Json(json!({
|
(StatusCode::OK, Json(json!({
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Item moved to trash successfully"
|
"message": "Item moved to trash successfully"
|
||||||
})))
|
})))
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al mover elemento a papelera: {:?}", e);
|
error!("Error moving item to trash: {:?}", e);
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||||
"error": format!("Error moving item to trash: {}", e)
|
"error": format!("Error moving item to trash: {}", e)
|
||||||
})))
|
})))
|
||||||
@@ -83,14 +83,14 @@ pub async fn move_to_trash(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mueve un archivo a la papelera
|
/// Moves a file to the trash
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn move_file_to_trash(
|
pub async fn move_file_to_trash(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(item_id): Path<String>,
|
Path(item_id): Path<String>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
debug!("Solicitud para mover archivo a papelera: id={}, usuario={}",
|
debug!("Request to move file to trash: id={}, user={}",
|
||||||
item_id, auth_user.id);
|
item_id, auth_user.id);
|
||||||
|
|
||||||
let trash_service = match state.trash_service.as_ref() {
|
let trash_service = match state.trash_service.as_ref() {
|
||||||
@@ -102,19 +102,19 @@ pub async fn move_file_to_trash(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Especificar que es un archivo
|
// Specify that it is a file
|
||||||
let result = trash_service.move_to_trash(&item_id, "file", &auth_user.id).await;
|
let result = trash_service.move_to_trash(&item_id, "file", &auth_user.id).await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Archivo movido a papelera con éxito");
|
debug!("File moved to trash successfully");
|
||||||
(StatusCode::OK, Json(json!({
|
(StatusCode::OK, Json(json!({
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "File moved to trash successfully"
|
"message": "File moved to trash successfully"
|
||||||
})))
|
})))
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al mover archivo a papelera: {:?}", e);
|
error!("Error moving file to trash: {:?}", e);
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||||
"error": format!("Error moving file to trash: {}", e)
|
"error": format!("Error moving file to trash: {}", e)
|
||||||
})))
|
})))
|
||||||
@@ -122,14 +122,14 @@ pub async fn move_file_to_trash(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mueve una carpeta a la papelera
|
/// Moves a folder to the trash
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn move_folder_to_trash(
|
pub async fn move_folder_to_trash(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(item_id): Path<String>,
|
Path(item_id): Path<String>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
debug!("Solicitud para mover carpeta a papelera: id={}, usuario={}",
|
debug!("Request to move folder to trash: id={}, user={}",
|
||||||
item_id, auth_user.id);
|
item_id, auth_user.id);
|
||||||
|
|
||||||
let trash_service = match state.trash_service.as_ref() {
|
let trash_service = match state.trash_service.as_ref() {
|
||||||
@@ -141,19 +141,19 @@ pub async fn move_folder_to_trash(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Especificar que es una carpeta
|
// Specify that it is a folder
|
||||||
let result = trash_service.move_to_trash(&item_id, "folder", &auth_user.id).await;
|
let result = trash_service.move_to_trash(&item_id, "folder", &auth_user.id).await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Carpeta movida a papelera con éxito");
|
debug!("Folder moved to trash successfully");
|
||||||
(StatusCode::OK, Json(json!({
|
(StatusCode::OK, Json(json!({
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Folder moved to trash successfully"
|
"message": "Folder moved to trash successfully"
|
||||||
})))
|
})))
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al mover carpeta a papelera: {:?}", e);
|
error!("Error moving folder to trash: {:?}", e);
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||||
"error": format!("Error moving folder to trash: {}", e)
|
"error": format!("Error moving folder to trash: {}", e)
|
||||||
})))
|
})))
|
||||||
@@ -161,14 +161,14 @@ pub async fn move_folder_to_trash(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restaura un elemento desde la papelera a su ubicación original
|
/// Restores an item from the trash to its original location
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn restore_from_trash(
|
pub async fn restore_from_trash(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(trash_id): Path<String>,
|
Path(trash_id): Path<String>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
debug!("Solicitud para restaurar elemento {} de papelera", trash_id);
|
debug!("Request to restore item {} from trash", trash_id);
|
||||||
|
|
||||||
let trash_service = match state.trash_service.as_ref() {
|
let trash_service = match state.trash_service.as_ref() {
|
||||||
Some(service) => service,
|
Some(service) => service,
|
||||||
@@ -182,7 +182,7 @@ pub async fn restore_from_trash(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Elemento restaurado con éxito");
|
debug!("Item restored successfully");
|
||||||
(StatusCode::OK, Json(json!({
|
(StatusCode::OK, Json(json!({
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Item restored successfully"
|
"message": "Item restored successfully"
|
||||||
@@ -199,7 +199,7 @@ pub async fn restore_from_trash(
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
error!("Error al restaurar elemento de papelera: {:?}", e);
|
error!("Error restoring item from trash: {:?}", e);
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||||
"error": format!("Error restoring item from trash: {}", e)
|
"error": format!("Error restoring item from trash: {}", e)
|
||||||
})))
|
})))
|
||||||
@@ -207,14 +207,14 @@ pub async fn restore_from_trash(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina permanentemente un elemento de la papelera
|
/// Permanently deletes an item from the trash
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn delete_permanently(
|
pub async fn delete_permanently(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(trash_id): Path<String>,
|
Path(trash_id): Path<String>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
debug!("Solicitud para eliminar permanentemente elemento {}", trash_id);
|
debug!("Request to permanently delete item {}", trash_id);
|
||||||
|
|
||||||
let trash_service = match state.trash_service.as_ref() {
|
let trash_service = match state.trash_service.as_ref() {
|
||||||
Some(service) => service,
|
Some(service) => service,
|
||||||
@@ -228,7 +228,7 @@ pub async fn delete_permanently(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Elemento eliminado permanentemente");
|
debug!("Item permanently deleted");
|
||||||
(StatusCode::OK, Json(json!({
|
(StatusCode::OK, Json(json!({
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Item deleted permanently"
|
"message": "Item deleted permanently"
|
||||||
@@ -245,7 +245,7 @@ pub async fn delete_permanently(
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
error!("Error al eliminar permanentemente elemento: {:?}", e);
|
error!("Error permanently deleting item: {:?}", e);
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||||
"error": format!("Error deleting item permanently: {}", e)
|
"error": format!("Error deleting item permanently: {}", e)
|
||||||
})))
|
})))
|
||||||
@@ -253,13 +253,13 @@ pub async fn delete_permanently(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vacía la papelera completamente para el usuario actual
|
/// Empties the trash completely for the current user
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn empty_trash(
|
pub async fn empty_trash(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
debug!("Solicitud para vaciar papelera del usuario {}", auth_user.id);
|
debug!("Request to empty trash for user {}", auth_user.id);
|
||||||
|
|
||||||
let trash_service = match state.trash_service.as_ref() {
|
let trash_service = match state.trash_service.as_ref() {
|
||||||
Some(service) => service,
|
Some(service) => service,
|
||||||
@@ -273,14 +273,14 @@ pub async fn empty_trash(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Papelera vaciada con éxito");
|
debug!("Trash emptied successfully");
|
||||||
(StatusCode::OK, Json(json!({
|
(StatusCode::OK, Json(json!({
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Trash emptied successfully"
|
"message": "Trash emptied successfully"
|
||||||
})))
|
})))
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error al vaciar papelera: {:?}", e);
|
error!("Error emptying trash: {:?}", e);
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
|
||||||
"error": format!("Error emptying trash: {}", e)
|
"error": format!("Error emptying trash: {}", e)
|
||||||
})))
|
})))
|
||||||
|
|||||||
@@ -90,14 +90,14 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
|
|||||||
let favorites_service = app_state.favorites_service.clone();
|
let favorites_service = app_state.favorites_service.clone();
|
||||||
let recent_service = app_state.recent_service.clone();
|
let recent_service = app_state.recent_service.clone();
|
||||||
|
|
||||||
// Inicializar el servicio de operaciones por lotes
|
// Initialize the batch operations service
|
||||||
let batch_service = Arc::new(BatchOperationService::default(
|
let batch_service = Arc::new(BatchOperationService::default(
|
||||||
file_retrieval_service.clone(),
|
file_retrieval_service.clone(),
|
||||||
file_management_service.clone(),
|
file_management_service.clone(),
|
||||||
folder_service.clone()
|
folder_service.clone()
|
||||||
));
|
));
|
||||||
|
|
||||||
// Crear estado para el manejador de operaciones por lotes
|
// Create state for the batch operations handler
|
||||||
let batch_handler_state = BatchHandlerState {
|
let batch_handler_state = BatchHandlerState {
|
||||||
batch_service: batch_service.clone(),
|
batch_service: batch_service.clone(),
|
||||||
};
|
};
|
||||||
@@ -154,14 +154,14 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
|
|||||||
// Merge the routers
|
// Merge the routers
|
||||||
let files_router = basic_file_router.merge(file_operations_router);
|
let files_router = basic_file_router.merge(file_operations_router);
|
||||||
|
|
||||||
// Crear rutas para operaciones por lotes
|
// Create routes for batch operations
|
||||||
let batch_router = Router::new()
|
let batch_router = Router::new()
|
||||||
// Operaciones de archivos
|
// File operations
|
||||||
.route("/files/move", post(batch_handler::move_files_batch))
|
.route("/files/move", post(batch_handler::move_files_batch))
|
||||||
.route("/files/copy", post(batch_handler::copy_files_batch))
|
.route("/files/copy", post(batch_handler::copy_files_batch))
|
||||||
.route("/files/delete", post(batch_handler::delete_files_batch))
|
.route("/files/delete", post(batch_handler::delete_files_batch))
|
||||||
.route("/files/get", post(batch_handler::get_files_batch))
|
.route("/files/get", post(batch_handler::get_files_batch))
|
||||||
// Operaciones de carpetas
|
// Folder operations
|
||||||
.route("/folders/delete", post(batch_handler::delete_folders_batch))
|
.route("/folders/delete", post(batch_handler::delete_folders_batch))
|
||||||
.route("/folders/create", post(batch_handler::create_folders_batch))
|
.route("/folders/create", post(batch_handler::create_folders_batch))
|
||||||
.route("/folders/get", post(batch_handler::get_folders_batch))
|
.route("/folders/get", post(batch_handler::get_folders_batch))
|
||||||
@@ -183,7 +183,7 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
|
|||||||
Router::new()
|
Router::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Implementaciones directas de handlers para compartir, sin depender de ShareHandler
|
// Direct handler implementations for sharing, without depending on ShareHandler
|
||||||
|
|
||||||
// Create routes for shared resources management (requires auth)
|
// Create routes for shared resources management (requires auth)
|
||||||
let share_router = if let Some(share_service) = share_service.clone() {
|
let share_router = if let Some(share_service) = share_service.clone() {
|
||||||
|
|||||||
@@ -11,24 +11,24 @@ use crate::common::di::AppState;
|
|||||||
// Re-export CurrentUser from application layer for use in handlers
|
// Re-export CurrentUser from application layer for use in handlers
|
||||||
pub use crate::application::dtos::user_dto::CurrentUser;
|
pub use crate::application::dtos::user_dto::CurrentUser;
|
||||||
|
|
||||||
// Estructura para usar en extractores de Axum
|
// Structure for use in Axum extractors
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AuthUser {
|
pub struct AuthUser {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extractor reutilizable que obtiene el user_id del usuario autenticado.
|
/// Reusable extractor that gets the user_id of the authenticated user.
|
||||||
/// Se extrae automáticamente del `CurrentUser` insertado por el auth middleware.
|
/// Automatically extracted from the `CurrentUser` inserted by the auth middleware.
|
||||||
///
|
///
|
||||||
/// Uso en handlers:
|
/// Usage in handlers:
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
|
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CurrentUserId(pub String);
|
pub struct CurrentUserId(pub String);
|
||||||
|
|
||||||
// Implementar FromRequestParts para AuthUser — permite usar `auth_user: AuthUser` en handlers
|
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers
|
||||||
impl<S> FromRequestParts<S> for AuthUser
|
impl<S> FromRequestParts<S> for AuthUser
|
||||||
where
|
where
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
@@ -47,7 +47,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementar FromRequestParts para CurrentUserId — extractor ligero solo para el user_id
|
// Implement FromRequestParts for CurrentUserId — lightweight extractor for user_id only
|
||||||
impl<S> FromRequestParts<S> for CurrentUserId
|
impl<S> FromRequestParts<S> for CurrentUserId
|
||||||
where
|
where
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
@@ -63,37 +63,37 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error para las operaciones de autenticación
|
// Error for authentication operations
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum AuthError {
|
pub enum AuthError {
|
||||||
#[error("Token no proporcionado")]
|
#[error("Token not provided")]
|
||||||
TokenNotProvided,
|
TokenNotProvided,
|
||||||
|
|
||||||
#[error("Token inválido: {0}")]
|
#[error("Invalid token: {0}")]
|
||||||
InvalidToken(String),
|
InvalidToken(String),
|
||||||
|
|
||||||
#[error("Token expirado")]
|
#[error("Token expired")]
|
||||||
TokenExpired,
|
TokenExpired,
|
||||||
|
|
||||||
#[error("Usuario no encontrado")]
|
#[error("User not found")]
|
||||||
UserNotFound,
|
UserNotFound,
|
||||||
|
|
||||||
#[error("Acceso denegado: {0}")]
|
#[error("Access denied: {0}")]
|
||||||
AccessDenied(String),
|
AccessDenied(String),
|
||||||
|
|
||||||
#[error("Servicio de autenticación no disponible")]
|
#[error("Authentication service unavailable")]
|
||||||
AuthServiceUnavailable,
|
AuthServiceUnavailable,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for AuthError {
|
impl IntoResponse for AuthError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let (status, error_message) = match self {
|
let (status, error_message) = match self {
|
||||||
AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token no proporcionado".to_string()),
|
AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token not provided".to_string()),
|
||||||
AuthError::InvalidToken(msg) => (StatusCode::UNAUTHORIZED, msg),
|
AuthError::InvalidToken(msg) => (StatusCode::UNAUTHORIZED, msg),
|
||||||
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expirado".to_string()),
|
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expired".to_string()),
|
||||||
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "Usuario no encontrado".to_string()),
|
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "User not found".to_string()),
|
||||||
AuthError::AccessDenied(msg) => (StatusCode::FORBIDDEN, msg),
|
AuthError::AccessDenied(msg) => (StatusCode::FORBIDDEN, msg),
|
||||||
AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Servicio de autenticación no disponible".to_string()),
|
AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Authentication service unavailable".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = axum::Json(serde_json::json!({
|
let body = axum::Json(serde_json::json!({
|
||||||
@@ -104,24 +104,24 @@ impl IntoResponse for AuthError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Middleware de autenticación seguro.
|
/// Secure authentication middleware.
|
||||||
///
|
///
|
||||||
/// Valida el token JWT contra el servicio de autenticación configurado.
|
/// Validates the JWT token against the configured authentication service.
|
||||||
/// No acepta bypasses, tokens mock, ni parámetros de URL para saltar validación.
|
/// Does not accept bypasses, mock tokens, or URL parameters to skip validation.
|
||||||
pub async fn auth_middleware(
|
pub async fn auth_middleware(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
mut request: Request,
|
mut request: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Result<Response, AuthError> {
|
) -> Result<Response, AuthError> {
|
||||||
// Extraer el token Bearer del header Authorization
|
// Extract the Bearer token from the Authorization header
|
||||||
let token_str = headers
|
let token_str = headers
|
||||||
.get(header::AUTHORIZATION)
|
.get(header::AUTHORIZATION)
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.and_then(|value| value.strip_prefix("Bearer "))
|
.and_then(|value| value.strip_prefix("Bearer "))
|
||||||
.ok_or(AuthError::TokenNotProvided)?;
|
.ok_or(AuthError::TokenNotProvided)?;
|
||||||
|
|
||||||
// Validar que el token no esté vacío
|
// Validate that the token is not empty
|
||||||
let token_str = token_str.trim();
|
let token_str = token_str.trim();
|
||||||
if token_str.is_empty() {
|
if token_str.is_empty() {
|
||||||
return Err(AuthError::TokenNotProvided);
|
return Err(AuthError::TokenNotProvided);
|
||||||
@@ -129,7 +129,7 @@ pub async fn auth_middleware(
|
|||||||
|
|
||||||
tracing::debug!("Processing authentication token");
|
tracing::debug!("Processing authentication token");
|
||||||
|
|
||||||
// Validar el token usando el servicio de autenticación
|
// Validate the token using the authentication service
|
||||||
if let Some(auth_service) = state.auth_service.as_ref() {
|
if let Some(auth_service) = state.auth_service.as_ref() {
|
||||||
let token_service = &auth_service.token_service;
|
let token_service = &auth_service.token_service;
|
||||||
match token_service.validate_token(token_str) {
|
match token_service.validate_token(token_str) {
|
||||||
@@ -146,25 +146,25 @@ pub async fn auth_middleware(
|
|||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("Token validation failed: {}", e);
|
tracing::warn!("Token validation failed: {}", e);
|
||||||
return Err(AuthError::InvalidToken(format!("Token inválido: {}", e)));
|
return Err(AuthError::InvalidToken(format!("Invalid token: {}", e)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si no hay servicio de autenticación disponible, denegar acceso
|
// If no authentication service is available, deny access
|
||||||
tracing::error!("Auth middleware invoked but auth service is not configured");
|
tracing::error!("Auth middleware invoked but auth service is not configured");
|
||||||
Err(AuthError::AuthServiceUnavailable)
|
Err(AuthError::AuthServiceUnavailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Middleware para verificar que el usuario autenticado tiene rol de administrador.
|
/// Middleware to verify that the authenticated user has an admin role.
|
||||||
///
|
///
|
||||||
/// Debe aplicarse DESPUÉS del auth_middleware, ya que depende de que
|
/// Must be applied AFTER auth_middleware, as it depends on
|
||||||
/// `CurrentUser` esté presente en las extensiones de la request.
|
/// `CurrentUser` being present in the request extensions.
|
||||||
pub async fn require_admin(
|
pub async fn require_admin(
|
||||||
request: Request,
|
request: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
// Obtener el CurrentUser insertado por auth_middleware
|
// Get the CurrentUser inserted by auth_middleware
|
||||||
if let Some(current_user) = request.extensions().get::<CurrentUser>() {
|
if let Some(current_user) = request.extensions().get::<CurrentUser>() {
|
||||||
if current_user.role == "admin" {
|
if current_user.role == "admin" {
|
||||||
tracing::debug!("Admin access granted for user: {}", current_user.username);
|
tracing::debug!("Admin access granted for user: {}", current_user.username);
|
||||||
@@ -175,7 +175,7 @@ pub async fn require_admin(
|
|||||||
tracing::warn!("Admin check failed: no authenticated user in request");
|
tracing::warn!("Admin check failed: no authenticated user in request");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acceso denegado
|
// Access denied
|
||||||
let error = AuthError::AccessDenied("Se requiere rol de administrador".to_string());
|
let error = AuthError::AccessDenied("Admin role required".to_string());
|
||||||
error.into_response()
|
error.into_response()
|
||||||
}
|
}
|
||||||
@@ -17,39 +17,39 @@ use std::future::Future;
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
const MAX_CACHE_ENTRIES: usize = 1000; // Máximo número de entradas en caché
|
const MAX_CACHE_ENTRIES: usize = 1000; // Maximum number of cache entries
|
||||||
const DEFAULT_MAX_AGE: u64 = 60; // Tiempo de vida por defecto en segundos
|
const DEFAULT_MAX_AGE: u64 = 60; // Default time-to-live in seconds
|
||||||
|
|
||||||
// Definición de tipos para mayor claridad
|
// Type definitions for clarity
|
||||||
type CacheKey = String;
|
type CacheKey = String;
|
||||||
type EntityTag = String;
|
type EntityTag = String;
|
||||||
|
|
||||||
/// Un valor almacenado en caché
|
/// A cached value
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct CacheEntry {
|
struct CacheEntry {
|
||||||
/// El ETag calculado para este valor
|
/// The ETag calculated for this value
|
||||||
etag: EntityTag,
|
etag: EntityTag,
|
||||||
/// Los datos serializados en bytes
|
/// The serialized data in bytes
|
||||||
data: Option<Bytes>,
|
data: Option<Bytes>,
|
||||||
/// Las cabeceras originales
|
/// The original headers
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
/// Timestamp de cuando fue almacenado
|
/// Timestamp of when it was stored
|
||||||
timestamp: SystemTime,
|
timestamp: SystemTime,
|
||||||
/// Tiempo de vida en segundos
|
/// Time-to-live in seconds
|
||||||
max_age: u64,
|
max_age: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cache para respuestas HTTP con soporte para ETag
|
/// Cache for HTTP responses with ETag support
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct HttpCache {
|
pub struct HttpCache {
|
||||||
/// Almacenamiento de entradas en caché
|
/// Cache entry storage
|
||||||
cache: Arc<Mutex<HashMap<CacheKey, CacheEntry>>>,
|
cache: Arc<Mutex<HashMap<CacheKey, CacheEntry>>>,
|
||||||
/// Tiempo de vida por defecto para las entradas
|
/// Default time-to-live for entries
|
||||||
default_max_age: u64,
|
default_max_age: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpCache {
|
impl HttpCache {
|
||||||
/// Crea una nueva instancia del caché
|
/// Creates a new cache instance
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
|
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
|
||||||
@@ -57,7 +57,7 @@ impl HttpCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una nueva instancia con un tiempo de vida especificado
|
/// Creates a new instance with a specified time-to-live
|
||||||
pub fn with_max_age(max_age: u64) -> Self {
|
pub fn with_max_age(max_age: u64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
|
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
|
||||||
@@ -65,12 +65,12 @@ impl HttpCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene estadísticas del caché
|
/// Gets cache statistics
|
||||||
pub fn stats(&self) -> (usize, usize) {
|
pub fn stats(&self) -> (usize, usize) {
|
||||||
let lock = self.cache.lock().unwrap();
|
let lock = self.cache.lock().unwrap();
|
||||||
let total = lock.len();
|
let total = lock.len();
|
||||||
|
|
||||||
// Contar entradas válidas
|
// Count valid entries
|
||||||
let _now = SystemTime::now();
|
let _now = SystemTime::now();
|
||||||
let valid = lock.values().filter(|entry| {
|
let valid = lock.values().filter(|entry| {
|
||||||
match entry.timestamp.elapsed() {
|
match entry.timestamp.elapsed() {
|
||||||
@@ -82,12 +82,12 @@ impl HttpCache {
|
|||||||
(total, valid)
|
(total, valid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Limpia entradas expiradas
|
/// Cleans up expired entries
|
||||||
pub fn cleanup(&self) -> usize {
|
pub fn cleanup(&self) -> usize {
|
||||||
let mut lock = self.cache.lock().unwrap();
|
let mut lock = self.cache.lock().unwrap();
|
||||||
let initial_count = lock.len();
|
let initial_count = lock.len();
|
||||||
|
|
||||||
// Eliminar entradas expiradas
|
// Remove expired entries
|
||||||
let _now = SystemTime::now();
|
let _now = SystemTime::now();
|
||||||
lock.retain(|_, entry| {
|
lock.retain(|_, entry| {
|
||||||
match entry.timestamp.elapsed() {
|
match entry.timestamp.elapsed() {
|
||||||
@@ -102,18 +102,18 @@ impl HttpCache {
|
|||||||
removed
|
removed
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece una entrada en el caché
|
/// Sets an entry in the cache
|
||||||
fn set(&self, key: &str, etag: EntityTag, data: Option<Bytes>, headers: HeaderMap, max_age: Option<u64>) {
|
fn set(&self, key: &str, etag: EntityTag, data: Option<Bytes>, headers: HeaderMap, max_age: Option<u64>) {
|
||||||
let mut lock = self.cache.lock().unwrap();
|
let mut lock = self.cache.lock().unwrap();
|
||||||
|
|
||||||
// Aplicar política de eviction si el caché está lleno
|
// Apply eviction policy if the cache is full
|
||||||
if lock.len() >= MAX_CACHE_ENTRIES {
|
if lock.len() >= MAX_CACHE_ENTRIES {
|
||||||
debug!("Cache full, removing oldest entries");
|
debug!("Cache full, removing oldest entries");
|
||||||
// Eliminar el 10% de las entradas más antiguas
|
// Remove the oldest 10% of entries
|
||||||
self.evict_oldest(&mut lock, MAX_CACHE_ENTRIES / 10);
|
self.evict_oldest(&mut lock, MAX_CACHE_ENTRIES / 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Almacenar la nueva entrada
|
// Store the new entry
|
||||||
lock.insert(key.to_string(), CacheEntry {
|
lock.insert(key.to_string(), CacheEntry {
|
||||||
etag,
|
etag,
|
||||||
data,
|
data,
|
||||||
@@ -123,30 +123,30 @@ impl HttpCache {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina las entradas más antiguas del caché
|
/// Removes the oldest entries from the cache
|
||||||
fn evict_oldest(&self, cache: &mut HashMap<CacheKey, CacheEntry>, count: usize) {
|
fn evict_oldest(&self, cache: &mut HashMap<CacheKey, CacheEntry>, count: usize) {
|
||||||
// Ordenar por timestamp
|
// Sort by timestamp
|
||||||
let mut entries: Vec<(CacheKey, SystemTime)> = cache
|
let mut entries: Vec<(CacheKey, SystemTime)> = cache
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(key, entry)| (key.clone(), entry.timestamp))
|
.map(|(key, entry)| (key.clone(), entry.timestamp))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Ordenar por timestamp (más antiguo primero)
|
// Sort by timestamp (oldest first)
|
||||||
entries.sort_by(|a, b| a.1.cmp(&b.1));
|
entries.sort_by(|a, b| a.1.cmp(&b.1));
|
||||||
|
|
||||||
// Eliminar las entradas más antiguas
|
// Remove the oldest entries
|
||||||
for (key, _) in entries.iter().take(count) {
|
for (key, _) in entries.iter().take(count) {
|
||||||
cache.remove(key);
|
cache.remove(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtiene una entrada del caché
|
/// Gets an entry from the cache
|
||||||
fn get(&self, key: &str) -> Option<CacheEntry> {
|
fn get(&self, key: &str) -> Option<CacheEntry> {
|
||||||
let lock = self.cache.lock().unwrap();
|
let lock = self.cache.lock().unwrap();
|
||||||
|
|
||||||
// Buscar la entrada
|
// Look up the entry
|
||||||
if let Some(entry) = lock.get(key) {
|
if let Some(entry) = lock.get(key) {
|
||||||
// Verificar si ha expirado
|
// Check if it has expired
|
||||||
match entry.timestamp.elapsed() {
|
match entry.timestamp.elapsed() {
|
||||||
Ok(elapsed) if elapsed.as_secs() < entry.max_age => {
|
Ok(elapsed) if elapsed.as_secs() < entry.max_age => {
|
||||||
// Entry is still valid
|
// Entry is still valid
|
||||||
@@ -162,9 +162,9 @@ impl HttpCache {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Genera un ETag simple para un bloque de bytes
|
/// Generates a simple ETag for a block of bytes
|
||||||
fn calculate_etag_for_bytes(&self, bytes: &[u8]) -> EntityTag {
|
fn calculate_etag_for_bytes(&self, bytes: &[u8]) -> EntityTag {
|
||||||
// Calcular hash
|
// Calculate hash
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
bytes.hash(&mut hasher);
|
bytes.hash(&mut hasher);
|
||||||
let hash = hasher.finish();
|
let hash = hasher.finish();
|
||||||
@@ -173,7 +173,7 @@ impl HttpCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Middleware de caché HTTP
|
/// HTTP cache middleware
|
||||||
pub async fn cache_middleware<T>(
|
pub async fn cache_middleware<T>(
|
||||||
cache: HttpCache,
|
cache: HttpCache,
|
||||||
cache_key: &str,
|
cache_key: &str,
|
||||||
@@ -184,65 +184,65 @@ pub async fn cache_middleware<T>(
|
|||||||
where
|
where
|
||||||
T: Serialize
|
T: Serialize
|
||||||
{
|
{
|
||||||
// Solo aplicar caché para solicitudes GET
|
// Only apply cache for GET requests
|
||||||
if req.method() != Method::GET {
|
if req.method() != Method::GET {
|
||||||
return Ok(next.run(req).await);
|
return Ok(next.run(req).await);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar si la respuesta está en caché
|
// Check if the response is cached
|
||||||
let if_none_match = req.headers()
|
let if_none_match = req.headers()
|
||||||
.get("if-none-match")
|
.get("if-none-match")
|
||||||
.and_then(|v| v.to_str().ok());
|
.and_then(|v| v.to_str().ok());
|
||||||
|
|
||||||
// Si hay una entrada en caché
|
// If there is a cache entry
|
||||||
if let Some(cache_entry) = cache.get(cache_key) {
|
if let Some(cache_entry) = cache.get(cache_key) {
|
||||||
// Comprobar si el cliente ya tiene la versión actualizada
|
// Check if the client already has the updated version
|
||||||
if let Some(client_etag) = if_none_match {
|
if let Some(client_etag) = if_none_match {
|
||||||
if client_etag == cache_entry.etag {
|
if client_etag == cache_entry.etag {
|
||||||
// El cliente tiene la versión más reciente, enviar 304 Not Modified
|
// The client has the most recent version, send 304 Not Modified
|
||||||
debug!("Cache hit (304) for key: {}", cache_key);
|
debug!("Cache hit (304) for key: {}", cache_key);
|
||||||
return Ok(create_not_modified_response(&cache_entry));
|
return Ok(create_not_modified_response(&cache_entry));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// El cliente necesita la versión actualizada
|
// The client needs the updated version
|
||||||
if let Some(data) = &cache_entry.data {
|
if let Some(data) = &cache_entry.data {
|
||||||
debug!("Cache hit (200) for key: {}", cache_key);
|
debug!("Cache hit (200) for key: {}", cache_key);
|
||||||
|
|
||||||
// Crear respuesta con los datos en caché
|
// Create response with cached data
|
||||||
let mut response = Response::new(Body::from(data.clone()));
|
let mut response = Response::new(Body::from(data.clone()));
|
||||||
|
|
||||||
// Copiar cabeceras originales
|
// Copy original headers
|
||||||
for (key, value) in &cache_entry.headers {
|
for (key, value) in &cache_entry.headers {
|
||||||
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
|
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
|
||||||
response.headers_mut().insert(key.clone(), value.clone());
|
response.headers_mut().insert(key.clone(), value.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Añadir cabeceras de caché
|
// Add cache headers
|
||||||
set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
|
set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
|
||||||
|
|
||||||
return Ok(response);
|
return Ok(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No está en caché o ha expirado, continuar con el middleware
|
// Not cached or expired, continue with the middleware
|
||||||
debug!("Cache miss for key: {}", cache_key);
|
debug!("Cache miss for key: {}", cache_key);
|
||||||
let response = next.run(req).await;
|
let response = next.run(req).await;
|
||||||
|
|
||||||
// No cachear errores
|
// Don't cache errors
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Ok(response);
|
return Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convertir la respuesta para calcular el ETag
|
// Convert the response to calculate the ETag
|
||||||
let (parts, _body) = response.into_parts();
|
let (parts, _body) = response.into_parts();
|
||||||
let bytes = axum::body::to_bytes(_body, 1024 * 1024 * 10).await.unwrap_or_default();
|
let bytes = axum::body::to_bytes(_body, 1024 * 1024 * 10).await.unwrap_or_default();
|
||||||
|
|
||||||
// Calcular ETag
|
// Calculate ETag
|
||||||
let etag = cache.calculate_etag_for_bytes(&bytes);
|
let etag = cache.calculate_etag_for_bytes(&bytes);
|
||||||
|
|
||||||
// Guardar en caché
|
// Save to cache
|
||||||
cache.set(
|
cache.set(
|
||||||
cache_key,
|
cache_key,
|
||||||
etag.clone(),
|
etag.clone(),
|
||||||
@@ -251,26 +251,26 @@ where
|
|||||||
max_age
|
max_age
|
||||||
);
|
);
|
||||||
|
|
||||||
// Crear la respuesta con ETag
|
// Create the response with ETag
|
||||||
let mut response = Response::from_parts(parts, Body::from(bytes));
|
let mut response = Response::from_parts(parts, Body::from(bytes));
|
||||||
set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache.default_max_age));
|
set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache.default_max_age));
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una respuesta 304 Not Modified
|
/// Creates a 304 Not Modified response
|
||||||
fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> {
|
fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> {
|
||||||
let mut response = Response::builder()
|
let mut response = Response::builder()
|
||||||
.status(StatusCode::NOT_MODIFIED)
|
.status(StatusCode::NOT_MODIFIED)
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Copiar cabeceras de caché
|
// Copy cache headers
|
||||||
if let Some(cache_control) = entry.headers.get("cache-control") {
|
if let Some(cache_control) = entry.headers.get("cache-control") {
|
||||||
response.headers_mut().insert("cache-control", cache_control.clone());
|
response.headers_mut().insert("cache-control", cache_control.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Añadir ETag
|
// Add ETag
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
"etag",
|
"etag",
|
||||||
HeaderValue::from_str(&entry.etag).unwrap_or(HeaderValue::from_static(""))
|
HeaderValue::from_str(&entry.etag).unwrap_or(HeaderValue::from_static(""))
|
||||||
@@ -279,22 +279,22 @@ fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> {
|
|||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configura las cabeceras de caché para una respuesta
|
/// Configures cache headers for a response
|
||||||
fn set_cache_headers(response: &mut Response<Body>, etag: &str, max_age: u64) {
|
fn set_cache_headers(response: &mut Response<Body>, etag: &str, max_age: u64) {
|
||||||
// Añadir ETag
|
// Add ETag
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
"etag",
|
"etag",
|
||||||
HeaderValue::from_str(etag).unwrap_or(HeaderValue::from_static(""))
|
HeaderValue::from_str(etag).unwrap_or(HeaderValue::from_static(""))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Configurar Cache-Control
|
// Configure Cache-Control
|
||||||
let cache_control = format!("public, max-age={}", max_age);
|
let cache_control = format!("public, max-age={}", max_age);
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
"cache-control",
|
"cache-control",
|
||||||
HeaderValue::from_str(&cache_control).unwrap_or(HeaderValue::from_static(""))
|
HeaderValue::from_str(&cache_control).unwrap_or(HeaderValue::from_static(""))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Añadir cabecera Last-Modified
|
// Add Last-Modified header
|
||||||
let now: DateTime<Utc> = Utc::now();
|
let now: DateTime<Utc> = Utc::now();
|
||||||
let last_modified = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
|
let last_modified = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
@@ -303,7 +303,7 @@ fn set_cache_headers(response: &mut Response<Body>, etag: &str, max_age: u64) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layer para aplicar middleware de caché
|
/// Layer for applying cache middleware
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct HttpCacheLayer {
|
pub struct HttpCacheLayer {
|
||||||
cache: HttpCache,
|
cache: HttpCache,
|
||||||
@@ -311,7 +311,7 @@ pub struct HttpCacheLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HttpCacheLayer {
|
impl HttpCacheLayer {
|
||||||
/// Crea una nueva capa de caché
|
/// Creates a new cache layer
|
||||||
pub fn new(cache: HttpCache) -> Self {
|
pub fn new(cache: HttpCache) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cache,
|
cache,
|
||||||
@@ -319,7 +319,7 @@ impl HttpCacheLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece el tiempo de vida máximo
|
/// Sets the maximum time-to-live
|
||||||
pub fn with_max_age(mut self, max_age: u64) -> Self {
|
pub fn with_max_age(mut self, max_age: u64) -> Self {
|
||||||
self.max_age = Some(max_age);
|
self.max_age = Some(max_age);
|
||||||
self
|
self
|
||||||
@@ -338,7 +338,7 @@ impl<S> Layer<S> for HttpCacheLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Servicio que implementa la lógica de caché
|
/// Service that implements cache logic
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct HttpCacheService<S> {
|
pub struct HttpCacheService<S> {
|
||||||
inner: S,
|
inner: S,
|
||||||
@@ -365,10 +365,10 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
|
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
|
||||||
// Generar clave de caché
|
// Generate cache key
|
||||||
let cache_key = req.uri().path().to_string();
|
let cache_key = req.uri().path().to_string();
|
||||||
|
|
||||||
// Solo aplicar caché para solicitudes GET
|
// Only apply cache for GET requests
|
||||||
if req.method() != Method::GET {
|
if req.method() != Method::GET {
|
||||||
let future = self.inner.call(req);
|
let future = self.inner.call(req);
|
||||||
return Box::pin(async move {
|
return Box::pin(async move {
|
||||||
@@ -377,42 +377,42 @@ where
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtener ETag del cliente
|
// Get client ETag
|
||||||
let if_none_match = req.headers()
|
let if_none_match = req.headers()
|
||||||
.get("if-none-match")
|
.get("if-none-match")
|
||||||
.and_then(|v| v.to_str().ok());
|
.and_then(|v| v.to_str().ok());
|
||||||
|
|
||||||
// Verificar si hay una entrada en caché
|
// Check if there is a cache entry
|
||||||
let cache_clone = self.cache.clone();
|
let cache_clone = self.cache.clone();
|
||||||
let max_age = self.max_age;
|
let max_age = self.max_age;
|
||||||
let entry = cache_clone.get(&cache_key);
|
let entry = cache_clone.get(&cache_key);
|
||||||
|
|
||||||
match entry {
|
match entry {
|
||||||
Some(cache_entry) if if_none_match == Some(&cache_entry.etag) => {
|
Some(cache_entry) if if_none_match == Some(&cache_entry.etag) => {
|
||||||
// El cliente tiene la versión correcta, enviar 304
|
// The client has the correct version, send 304
|
||||||
debug!("Cache HIT (304): {}", cache_key);
|
debug!("Cache HIT (304): {}", cache_key);
|
||||||
let response = create_not_modified_response(&cache_entry);
|
let response = create_not_modified_response(&cache_entry);
|
||||||
return Box::pin(async move { Ok(response) });
|
return Box::pin(async move { Ok(response) });
|
||||||
},
|
},
|
||||||
Some(cache_entry) if cache_entry.data.is_some() => {
|
Some(cache_entry) if cache_entry.data.is_some() => {
|
||||||
// El cliente necesita la versión actualizada
|
// The client needs the updated version
|
||||||
debug!("Cache HIT (200): {}", cache_key);
|
debug!("Cache HIT (200): {}", cache_key);
|
||||||
let mut response = Response::new(Body::from(cache_entry.data.clone().unwrap()));
|
let mut response = Response::new(Body::from(cache_entry.data.clone().unwrap()));
|
||||||
|
|
||||||
// Copiar cabeceras originales
|
// Copy original headers
|
||||||
for (key, value) in &cache_entry.headers {
|
for (key, value) in &cache_entry.headers {
|
||||||
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
|
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
|
||||||
response.headers_mut().insert(key.clone(), value.clone());
|
response.headers_mut().insert(key.clone(), value.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Añadir cabeceras de caché
|
// Add cache headers
|
||||||
set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
|
set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
|
||||||
|
|
||||||
return Box::pin(async move { Ok(response) });
|
return Box::pin(async move { Ok(response) });
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
// No está en caché o ha expirado
|
// Not cached or expired
|
||||||
debug!("Cache MISS: {}", cache_key);
|
debug!("Cache MISS: {}", cache_key);
|
||||||
let future = self.inner.call(req);
|
let future = self.inner.call(req);
|
||||||
let cache_clone = self.cache.clone();
|
let cache_clone = self.cache.clone();
|
||||||
@@ -423,19 +423,19 @@ where
|
|||||||
let response = future.await.map_err(|e| e.into())?;
|
let response = future.await.map_err(|e| e.into())?;
|
||||||
let response = response_map_body(response).await;
|
let response = response_map_body(response).await;
|
||||||
|
|
||||||
// No cachear errores
|
// Don't cache errors
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Ok(response);
|
return Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtener el cuerpo y calcular ETag
|
// Get the body and calculate ETag
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
let bytes = axum::body::to_bytes(body, 1024 * 1024 * 10).await?;
|
let bytes = axum::body::to_bytes(body, 1024 * 1024 * 10).await?;
|
||||||
|
|
||||||
// Calcular ETag
|
// Calculate ETag
|
||||||
let etag = cache_clone.calculate_etag_for_bytes(&bytes);
|
let etag = cache_clone.calculate_etag_for_bytes(&bytes);
|
||||||
|
|
||||||
// Guardar en caché
|
// Save to cache
|
||||||
cache_clone.set(
|
cache_clone.set(
|
||||||
&cache_key,
|
&cache_key,
|
||||||
etag.clone(),
|
etag.clone(),
|
||||||
@@ -444,7 +444,7 @@ where
|
|||||||
max_age
|
max_age
|
||||||
);
|
);
|
||||||
|
|
||||||
// Crear la respuesta con ETag
|
// Create the response with ETag
|
||||||
let mut response = Response::from_parts(parts, Body::from(bytes));
|
let mut response = Response::from_parts(parts, Body::from(bytes));
|
||||||
set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache_clone.default_max_age));
|
set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache_clone.default_max_age));
|
||||||
|
|
||||||
@@ -455,9 +455,9 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Función auxiliar para convertir cualquier cuerpo en Body preservando su contenido.
|
// Helper function to convert any body into Body preserving its content.
|
||||||
// Anteriormente esta función descartaba el body con Body::empty(), causando
|
// Previously this function discarded the body with Body::empty(), causing
|
||||||
// pérdida de datos en respuestas no cacheadas.
|
// data loss in non-cached responses.
|
||||||
async fn response_map_body<B>(response: Response<B>) -> Response<Body>
|
async fn response_map_body<B>(response: Response<B>) -> Response<Body>
|
||||||
where
|
where
|
||||||
B: http_body::Body + Send + 'static,
|
B: http_body::Body + Send + 'static,
|
||||||
@@ -478,10 +478,10 @@ where
|
|||||||
Response::from_parts(parts, Body::from(collected))
|
Response::from_parts(parts, Body::from(collected))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inicia una tarea de limpieza periódica para el caché
|
/// Starts a periodic cleanup task for the cache
|
||||||
pub fn start_cache_cleanup_task(cache: HttpCache) {
|
pub fn start_cache_cleanup_task(cache: HttpCache) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(300)); // Cada 5 minutos
|
let mut interval = tokio::time::interval(Duration::from_secs(300)); // Every 5 minutes
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
@@ -516,10 +516,10 @@ mod tests {
|
|||||||
let etag2 = cache.calculate_etag_for_bytes(&data2);
|
let etag2 = cache.calculate_etag_for_bytes(&data2);
|
||||||
let etag3 = cache.calculate_etag_for_bytes(&data3);
|
let etag3 = cache.calculate_etag_for_bytes(&data3);
|
||||||
|
|
||||||
// Mismos datos deben generar mismo ETag
|
// Same data should generate the same ETag
|
||||||
assert_eq!(etag1, etag2);
|
assert_eq!(etag1, etag2);
|
||||||
|
|
||||||
// Datos diferentes deben generar ETags diferentes
|
// Different data should generate different ETags
|
||||||
assert_ne!(etag1, etag3);
|
assert_ne!(etag1, etag3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,19 +527,19 @@ mod tests {
|
|||||||
async fn test_cache_hit_miss() {
|
async fn test_cache_hit_miss() {
|
||||||
let cache = HttpCache::new();
|
let cache = HttpCache::new();
|
||||||
|
|
||||||
// Crear datos de prueba directamente como Bytes
|
// Create test data directly as Bytes
|
||||||
let bytes1 = Bytes::from(r#"{"id":1,"name":"Test"}"#);
|
let bytes1 = Bytes::from(r#"{"id":1,"name":"Test"}"#);
|
||||||
let headers1 = HeaderMap::new();
|
let headers1 = HeaderMap::new();
|
||||||
|
|
||||||
let etag1 = cache.calculate_etag_for_bytes(&bytes1);
|
let etag1 = cache.calculate_etag_for_bytes(&bytes1);
|
||||||
cache.set("test", etag1.clone(), Some(bytes1.clone()), headers1, None);
|
cache.set("test", etag1.clone(), Some(bytes1.clone()), headers1, None);
|
||||||
|
|
||||||
// Verificar cache hit
|
// Verify cache hit
|
||||||
let entry = cache.get("test").unwrap();
|
let entry = cache.get("test").unwrap();
|
||||||
assert_eq!(entry.etag, etag1);
|
assert_eq!(entry.etag, etag1);
|
||||||
assert_eq!(entry.data.unwrap(), bytes1);
|
assert_eq!(entry.data.unwrap(), bytes1);
|
||||||
|
|
||||||
// Verificar cache miss
|
// Verify cache miss
|
||||||
assert!(cache.get("nonexistent").is_none());
|
assert!(cache.get("nonexistent").is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
// Exportar los módulos principales del proyecto
|
// Export the main project modules
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod domain;
|
pub mod domain;
|
||||||
pub mod application;
|
pub mod application;
|
||||||
pub mod infrastructure;
|
pub mod infrastructure;
|
||||||
pub mod interfaces;
|
pub mod interfaces;
|
||||||
|
|
||||||
// Re-exportaciones públicas comunes
|
// Common public re-exports
|
||||||
pub use application::services::folder_service::FolderService;
|
pub use application::services::folder_service::FolderService;
|
||||||
pub use application::services::i18n_application_service::I18nApplicationService;
|
pub use application::services::i18n_application_service::I18nApplicationService;
|
||||||
pub use application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator};
|
pub use application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* Estilos para la funcionalidad de favoritos */
|
/* Styles for the favorites feature */
|
||||||
|
|
||||||
/* Indicador de favorito */
|
/* Favorite indicator */
|
||||||
.favorite-indicator {
|
.favorite-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
@@ -31,12 +31,12 @@
|
|||||||
text-shadow: 0 0 5px rgba(255, 193, 7, 0.5);
|
text-shadow: 0 0 5px rgba(255, 193, 7, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos para los elementos de la vista de favoritos */
|
/* Styles for the favorites view items */
|
||||||
.favorite-item {
|
.favorite-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ajustes para la vista de cuadrícula */
|
/* Adjustments for grid view */
|
||||||
.file-card.favorite-item {
|
.file-card.favorite-item {
|
||||||
border-left: 3px solid #ffc107;
|
border-left: 3px solid #ffc107;
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ajustes para la vista de lista */
|
/* Adjustments for list view */
|
||||||
.file-item.favorite-item {
|
.file-item.favorite-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px;
|
grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px;
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos para el estado vacío específico de favoritos */
|
/* Styles for the favorites-specific empty state */
|
||||||
.favorites-empty-state {
|
.favorites-empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animación para la estrella de favorito */
|
/* Animation for the favorite star */
|
||||||
@keyframes favorite-pulse {
|
@keyframes favorite-pulse {
|
||||||
0% { transform: scale(1); }
|
0% { transform: scale(1); }
|
||||||
50% { transform: scale(1.2); }
|
50% { transform: scale(1.2); }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* Estilos para la funcionalidad de archivos recientes */
|
/* Styles for the recent files feature */
|
||||||
|
|
||||||
/* Indicador de reciente */
|
/* Recent indicator */
|
||||||
.recent-indicator {
|
.recent-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
@@ -20,12 +20,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos para los elementos de la vista de recientes */
|
/* Styles for the recent view items */
|
||||||
.recent-item {
|
.recent-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ajustes para la vista de cuadrícula */
|
/* Adjustments for grid view */
|
||||||
.file-card.recent-item {
|
.file-card.recent-item {
|
||||||
border-left: 3px solid #6c757d;
|
border-left: 3px solid #6c757d;
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ajustes para la vista de lista */
|
/* Adjustments for list view */
|
||||||
.file-item.recent-item {
|
.file-item.recent-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px;
|
grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px;
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos para el estado vacío específico de recientes */
|
/* Styles for the recent-specific empty state */
|
||||||
.recents-empty-state {
|
.recents-empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -72,12 +72,12 @@
|
|||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tooltip para tiempo de acceso */
|
/* Tooltip for access time */
|
||||||
.recent-item .file-info {
|
.recent-item .file-info {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animación para archivos recientes */
|
/* Animation for recent files */
|
||||||
.recent-item {
|
.recent-item {
|
||||||
animation: recent-fade-in 0.3s ease;
|
animation: recent-fade-in 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -1271,7 +1271,7 @@ select:focus {
|
|||||||
.file-icon.doc-icon {
|
.file-icon.doc-icon {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
background-color: #e2e8f0; /* Fondo gris claro */
|
background-color: #e2e8f0; /* Light gray background */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -1436,7 +1436,7 @@ select:focus {
|
|||||||
.file-icon.code-icon {
|
.file-icon.code-icon {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
background-color: #e2e8f0; /* Fondo gris claro */
|
background-color: #e2e8f0; /* Light gray background */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -1770,7 +1770,7 @@ select:focus {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Lista de archivos - Estilo mejorado */
|
/* File list - Improved style */
|
||||||
.files-list-view {
|
.files-list-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1802,7 +1802,7 @@ select:focus {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Para modo papelera, ajustar columnas */
|
/* For trash mode, adjust columns */
|
||||||
.trash-item.file-item {
|
.trash-item.file-item {
|
||||||
grid-template-columns: minmax(180px, 1.5fr) 0.5fr 1fr 120px 100px;
|
grid-template-columns: minmax(180px, 1.5fr) 0.5fr 1fr 120px 100px;
|
||||||
}
|
}
|
||||||
@@ -1851,7 +1851,7 @@ select:focus {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Iconos específicos para archivos en vista de lista */
|
/* Specific icons for files in list view */
|
||||||
.file-item .file-icon.pdf-icon {
|
.file-item .file-icon.pdf-icon {
|
||||||
background-color: #fee2e2;
|
background-color: #fee2e2;
|
||||||
}
|
}
|
||||||
@@ -2129,7 +2129,7 @@ select:focus {
|
|||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Notificación */
|
/* Notification */
|
||||||
.notification {
|
.notification {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 70px;
|
top: 70px;
|
||||||
@@ -3327,7 +3327,7 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos específicos para la página Shared */
|
/* Specific styles for the Shared page */
|
||||||
.page-description {
|
.page-description {
|
||||||
color: #718096;
|
color: #718096;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -3335,7 +3335,7 @@ header {
|
|||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mejoras para los filtros en la página Shared */
|
/* Improvements for the Shared page filters */
|
||||||
.shared-filters {
|
.shared-filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -3383,7 +3383,7 @@ header {
|
|||||||
width: 250px;
|
width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mejoras en la tabla de elementos compartidos */
|
/* Improvements in the shared items table */
|
||||||
.shared-list-container {
|
.shared-list-container {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -3412,7 +3412,7 @@ header {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estado vacío mejorado */
|
/* Improved empty state */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
+28
-28
@@ -27,7 +27,7 @@
|
|||||||
<script src="/js/fileRenderer.js"></script>
|
<script src="/js/fileRenderer.js"></script>
|
||||||
<script src="/js/fileSharing.js"></script>
|
<script src="/js/fileSharing.js"></script>
|
||||||
<script src="/js/components/sharedView.js"></script>
|
<script src="/js/components/sharedView.js"></script>
|
||||||
<!-- Los viewers se cargan al final para asegurar que document.body esté disponible -->
|
<!-- Viewers are loaded last to ensure document.body is available -->
|
||||||
<script defer src="/js/fileViewer.js"></script>
|
<script defer src="/js/fileViewer.js"></script>
|
||||||
<script defer src="/js/inlineViewer.js"></script>
|
<script defer src="/js/inlineViewer.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<i class="fas fa-search search-icon"></i>
|
<i class="fas fa-search search-icon"></i>
|
||||||
<input type="text" id="search-input" data-i18n-placeholder="actions.search" placeholder="Buscar archivos, carpetas...">
|
<input type="text" id="search-input" data-i18n-placeholder="actions.search" placeholder="Search files, folders...">
|
||||||
<button id="search-button" class="search-button" data-i18n-title="actions.search_btn" title="Search">
|
<button id="search-button" class="search-button" data-i18n-title="actions.search_btn" title="Search">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -110,8 +110,8 @@
|
|||||||
<div class="user-menu-header">
|
<div class="user-menu-header">
|
||||||
<div class="user-menu-avatar" id="user-menu-avatar">AD</div>
|
<div class="user-menu-avatar" id="user-menu-avatar">AD</div>
|
||||||
<div class="user-menu-info">
|
<div class="user-menu-info">
|
||||||
<div class="user-menu-name" id="user-menu-name">Usuario</div>
|
<div class="user-menu-name" id="user-menu-name">User</div>
|
||||||
<div class="user-menu-email" id="user-menu-email">usuario@oxicloud.app</div>
|
<div class="user-menu-email" id="user-menu-email">user@oxicloud.app</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-menu-role-badge" id="user-menu-role-badge" style="display:none">
|
<div class="user-menu-role-badge" id="user-menu-role-badge" style="display:none">
|
||||||
@@ -120,38 +120,38 @@
|
|||||||
<div class="user-menu-storage">
|
<div class="user-menu-storage">
|
||||||
<div class="user-menu-storage-label">
|
<div class="user-menu-storage-label">
|
||||||
<i class="fas fa-database"></i>
|
<i class="fas fa-database"></i>
|
||||||
<span data-i18n="storage.title">Almacenamiento</span>
|
<span data-i18n="storage.title">Storage</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-menu-storage-bar">
|
<div class="user-menu-storage-bar">
|
||||||
<div class="user-menu-storage-fill" id="user-menu-storage-fill"></div>
|
<div class="user-menu-storage-fill" id="user-menu-storage-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-menu-storage-text" id="user-menu-storage-text">0% usado</div>
|
<div class="user-menu-storage-text" id="user-menu-storage-text">0% used</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-menu-divider"></div>
|
<div class="user-menu-divider"></div>
|
||||||
<button class="user-menu-item user-menu-admin" id="user-menu-admin" style="display:none">
|
<button class="user-menu-item user-menu-admin" id="user-menu-admin" style="display:none">
|
||||||
<i class="fas fa-cogs"></i>
|
<i class="fas fa-cogs"></i>
|
||||||
<span data-i18n="user_menu.admin_panel">Panel de administración</span>
|
<span data-i18n="user_menu.admin_panel">Admin panel</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="user-menu-item" id="user-menu-profile">
|
<button class="user-menu-item" id="user-menu-profile">
|
||||||
<i class="fas fa-user-circle"></i>
|
<i class="fas fa-user-circle"></i>
|
||||||
<span data-i18n="user_menu.profile">Mi perfil</span>
|
<span data-i18n="user_menu.profile">My profile</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="user-menu-divider" id="user-menu-admin-divider" style="display:none"></div>
|
<div class="user-menu-divider" id="user-menu-admin-divider" style="display:none"></div>
|
||||||
<button class="user-menu-item" id="user-menu-theme">
|
<button class="user-menu-item" id="user-menu-theme">
|
||||||
<i class="fas fa-moon"></i>
|
<i class="fas fa-moon"></i>
|
||||||
<span data-i18n="user_menu.appearance">Apariencia</span>
|
<span data-i18n="user_menu.appearance">Appearance</span>
|
||||||
<div class="theme-toggle-pill" id="theme-toggle-pill">
|
<div class="theme-toggle-pill" id="theme-toggle-pill">
|
||||||
<div class="theme-toggle-knob"></div>
|
<div class="theme-toggle-knob"></div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button class="user-menu-item" id="user-menu-about">
|
<button class="user-menu-item" id="user-menu-about">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
<span data-i18n="user_menu.about">Acerca de OxiCloud</span>
|
<span data-i18n="user_menu.about">About OxiCloud</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="user-menu-divider"></div>
|
<div class="user-menu-divider"></div>
|
||||||
<button class="user-menu-item user-menu-logout" id="user-menu-logout">
|
<button class="user-menu-item user-menu-logout" id="user-menu-logout">
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
<span data-i18n="actions.logout">Cerrar sesión</span>
|
<span data-i18n="actions.logout">Log out</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,38 +159,38 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<h1 class="page-title" data-i18n="nav.files">Archivos</h1>
|
<h1 class="page-title" data-i18n="nav.files">Files</h1>
|
||||||
|
|
||||||
<div class="actions-bar">
|
<div class="actions-bar">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<div class="upload-dropdown" id="upload-dropdown">
|
<div class="upload-dropdown" id="upload-dropdown">
|
||||||
<button class="btn btn-primary" id="upload-btn">
|
<button class="btn btn-primary" id="upload-btn">
|
||||||
<i class="fas fa-cloud-upload-alt"></i>
|
<i class="fas fa-cloud-upload-alt"></i>
|
||||||
<span data-i18n="actions.upload">Subir</span>
|
<span data-i18n="actions.upload">Upload</span>
|
||||||
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
|
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
|
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
|
||||||
<button class="upload-dropdown-item" id="upload-files-btn">
|
<button class="upload-dropdown-item" id="upload-files-btn">
|
||||||
<i class="fas fa-file"></i>
|
<i class="fas fa-file"></i>
|
||||||
<span data-i18n="actions.upload_files">Subir archivos</span>
|
<span data-i18n="actions.upload_files">Upload files</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="upload-dropdown-item" id="upload-folder-btn">
|
<button class="upload-dropdown-item" id="upload-folder-btn">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
<span data-i18n="actions.upload_folder">Subir carpeta</span>
|
<span data-i18n="actions.upload_folder">Upload folder</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary" id="new-folder-btn">
|
<button class="btn btn-secondary" id="new-folder-btn">
|
||||||
<i class="fas fa-folder-plus"></i>
|
<i class="fas fa-folder-plus"></i>
|
||||||
<span data-i18n="actions.new_folder">Nueva carpeta</span>
|
<span data-i18n="actions.new_folder">New folder</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
<button class="toggle-btn active" id="grid-view-btn" title="Vista de cuadrícula">
|
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
||||||
<i class="fas fa-th"></i>
|
<i class="fas fa-th"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="toggle-btn" id="list-view-btn" title="Vista de lista">
|
<button class="toggle-btn" id="list-view-btn" title="List view">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
|
|
||||||
<div class="dropzone" id="dropzone">
|
<div class="dropzone" id="dropzone">
|
||||||
<i class="fas fa-cloud-upload-alt" style="font-size: 32px; margin-bottom: 10px;"></i>
|
<i class="fas fa-cloud-upload-alt" style="font-size: 32px; margin-bottom: 10px;"></i>
|
||||||
<p data-i18n="dropzone.drag_files">Arrastra archivos aquí o haz clic para seleccionar</p>
|
<p data-i18n="dropzone.drag_files">Drag files here or click to select</p>
|
||||||
<input type="file" id="file-input" style="display: none;" multiple>
|
<input type="file" id="file-input" style="display: none;" multiple>
|
||||||
<input type="file" id="folder-input" style="display: none;" webkitdirectory directory multiple>
|
<input type="file" id="folder-input" style="display: none;" webkitdirectory directory multiple>
|
||||||
<div class="upload-progress">
|
<div class="upload-progress">
|
||||||
@@ -222,10 +222,10 @@
|
|||||||
<!-- List View (hidden by default) -->
|
<!-- List View (hidden by default) -->
|
||||||
<div class="files-list-view" id="files-list-view" style="display: none;">
|
<div class="files-list-view" id="files-list-view" style="display: none;">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div data-i18n="files.name">Nombre</div>
|
<div data-i18n="files.name">Name</div>
|
||||||
<div data-i18n="files.type">Tipo</div>
|
<div data-i18n="files.type">Type</div>
|
||||||
<div data-i18n="files.size">Tamaño</div>
|
<div data-i18n="files.size">Size</div>
|
||||||
<div data-i18n="files.modified">Modificado</div>
|
<div data-i18n="files.modified">Modified</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Files will be populated here in list view -->
|
<!-- Files will be populated here in list view -->
|
||||||
</div>
|
</div>
|
||||||
@@ -240,18 +240,18 @@
|
|||||||
<div class="modal-icon">
|
<div class="modal-icon">
|
||||||
<i id="modal-icon" class="fas fa-folder-plus"></i>
|
<i id="modal-icon" class="fas fa-folder-plus"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 id="modal-title" data-i18n="dialogs.new_folder_title">Nueva carpeta</h3>
|
<h3 id="modal-title" data-i18n="dialogs.new_folder_title">New folder</h3>
|
||||||
<button class="modal-close-btn" id="modal-close-btn">
|
<button class="modal-close-btn" id="modal-close-btn">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<label id="modal-label" for="modal-input" data-i18n="dialogs.folder_name">Nombre de la carpeta</label>
|
<label id="modal-label" for="modal-input" data-i18n="dialogs.folder_name">Folder name</label>
|
||||||
<input type="text" id="modal-input" class="modal-input" placeholder="" autocomplete="off">
|
<input type="text" id="modal-input" class="modal-input" placeholder="" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-secondary" id="modal-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
|
<button class="btn btn-secondary" id="modal-cancel-btn" data-i18n="actions.cancel">Cancel</button>
|
||||||
<button class="btn btn-primary" id="modal-confirm-btn" data-i18n="actions.confirm">Crear</button>
|
<button class="btn btn-primary" id="modal-confirm-btn" data-i18n="actions.confirm">Create</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,7 +281,7 @@
|
|||||||
<i class="fas fa-file-alt"></i> MIT License
|
<i class="fas fa-file-alt"></i> MIT License
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="about-close-btn" id="about-close-btn" data-i18n="actions.close">Cerrar</button>
|
<button class="about-close-btn" id="about-close-btn" data-i18n="actions.close">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+69
-69
@@ -197,7 +197,7 @@ function setupUserMenu() {
|
|||||||
// Theme switching could be expanded here in the future
|
// Theme switching could be expanded here in the future
|
||||||
window.ui.showNotification(
|
window.ui.showNotification(
|
||||||
dark ? '🌙' : '☀️',
|
dark ? '🌙' : '☀️',
|
||||||
dark ? 'Modo oscuro activado (próximamente)' : 'Modo claro activado'
|
dark ? 'Dark mode enabled (coming soon)' : 'Light mode enabled'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -476,12 +476,12 @@ function setupEventListeners() {
|
|||||||
if (filesListView) filesListView.style.display = app.currentView === 'list' ? 'block' : 'none';
|
if (filesListView) filesListView.style.display = app.currentView === 'list' ? 'block' : 'none';
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Papelera';
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Trash';
|
||||||
elements.actionsBar.innerHTML = `
|
elements.actionsBar.innerHTML = `
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="btn btn-danger" id="empty-trash-btn">
|
<button class="btn btn-danger" id="empty-trash-btn">
|
||||||
<i class="fas fa-trash-alt"></i>
|
<i class="fas fa-trash-alt"></i>
|
||||||
<span>${window.i18n ? window.i18n.t('trash.empty_trash') : 'Vaciar papelera'}</span>
|
<span>${window.i18n ? window.i18n.t('trash.empty_trash') : 'Empty trash'}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -519,35 +519,35 @@ function setupEventListeners() {
|
|||||||
app.currentSection = 'files';
|
app.currentSection = 'files';
|
||||||
|
|
||||||
// Reset UI
|
// Reset UI
|
||||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Archivos';
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
|
||||||
elements.actionsBar.innerHTML = `
|
elements.actionsBar.innerHTML = `
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<div class="upload-dropdown" id="upload-dropdown">
|
<div class="upload-dropdown" id="upload-dropdown">
|
||||||
<button class="btn btn-primary" id="upload-btn">
|
<button class="btn btn-primary" id="upload-btn">
|
||||||
<i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i>
|
<i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i>
|
||||||
<span data-i18n="actions.upload">Subir</span>
|
<span data-i18n="actions.upload">Upload</span>
|
||||||
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
|
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
|
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
|
||||||
<button class="upload-dropdown-item" id="upload-files-btn">
|
<button class="upload-dropdown-item" id="upload-files-btn">
|
||||||
<i class="fas fa-file"></i>
|
<i class="fas fa-file"></i>
|
||||||
<span data-i18n="actions.upload_files">Subir archivos</span>
|
<span data-i18n="actions.upload_files">Upload files</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="upload-dropdown-item" id="upload-folder-btn">
|
<button class="upload-dropdown-item" id="upload-folder-btn">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
<span data-i18n="actions.upload_folder">Subir carpeta</span>
|
<span data-i18n="actions.upload_folder">Upload folder</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary" id="new-folder-btn">
|
<button class="btn btn-secondary" id="new-folder-btn">
|
||||||
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">Nueva carpeta</span>
|
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">New folder</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
<button class="toggle-btn active" id="grid-view-btn" title="Vista de cuadrícula">
|
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
||||||
<i class="fas fa-th"></i>
|
<i class="fas fa-th"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="toggle-btn" id="list-view-btn" title="Vista de lista">
|
<button class="toggle-btn" id="list-view-btn" title="List view">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -623,14 +623,14 @@ function setupEventListeners() {
|
|||||||
*/
|
*/
|
||||||
async function loadFiles(options = {}) {
|
async function loadFiles(options = {}) {
|
||||||
try {
|
try {
|
||||||
console.log("Iniciando loadFiles() - cargando archivos...", options);
|
console.log("Starting loadFiles() - loading files...", options);
|
||||||
|
|
||||||
// Flag para forzar el refresco completo ignorando caché
|
// Flag to force complete refresh ignoring cache
|
||||||
const forceRefresh = options.forceRefresh || false;
|
const forceRefresh = options.forceRefresh || false;
|
||||||
|
|
||||||
// Prevenir múltiples solicitudes de carga simultáneas
|
// Prevent multiple simultaneous load requests
|
||||||
if (window.isLoadingFiles) {
|
if (window.isLoadingFiles) {
|
||||||
console.log("Ya hay una carga de archivos en progreso, ignorando solicitud");
|
console.log("A file load is already in progress, ignoring request");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,7 +640,7 @@ async function loadFiles(options = {}) {
|
|||||||
elements.filesGrid.innerHTML = `
|
elements.filesGrid.innerHTML = `
|
||||||
<div class="files-loading-spinner">
|
<div class="files-loading-spinner">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<span>${window.i18n ? window.i18n.t('files.loading') : 'Cargando archivos…'}</span>
|
<span>${window.i18n ? window.i18n.t('files.loading') : 'Loading files…'}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -651,12 +651,12 @@ async function loadFiles(options = {}) {
|
|||||||
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
||||||
if (userData.username) {
|
if (userData.username) {
|
||||||
// Find user's home folder
|
// Find user's home folder
|
||||||
console.log("Buscando carpeta de usuario para", userData.username);
|
console.log("Looking for user folder for", userData.username);
|
||||||
await findUserHomeFolder(userData.username);
|
await findUserHomeFolder(userData.username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agregar timestamp para evitar caché
|
// Add timestamp to avoid cache
|
||||||
const timestamp = new Date().getTime();
|
const timestamp = new Date().getTime();
|
||||||
let url;
|
let url;
|
||||||
|
|
||||||
@@ -667,7 +667,7 @@ async function loadFiles(options = {}) {
|
|||||||
url = `/api/folders/${app.userHomeFolderId}/contents?t=${timestamp}`;
|
url = `/api/folders/${app.userHomeFolderId}/contents?t=${timestamp}`;
|
||||||
app.currentPath = app.userHomeFolderId;
|
app.currentPath = app.userHomeFolderId;
|
||||||
ui.updateBreadcrumb(app.userHomeFolderName || 'Home');
|
ui.updateBreadcrumb(app.userHomeFolderName || 'Home');
|
||||||
console.log(`Cargando carpeta del usuario: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
|
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
|
||||||
} else {
|
} else {
|
||||||
// Emergency fallback - this should rarely happen but prevents errors
|
// Emergency fallback - this should rarely happen but prevents errors
|
||||||
url = `/api/folders?t=${timestamp}`;
|
url = `/api/folders?t=${timestamp}`;
|
||||||
@@ -676,7 +676,7 @@ async function loadFiles(options = {}) {
|
|||||||
} else {
|
} else {
|
||||||
// Normal case - viewing subfolder contents
|
// Normal case - viewing subfolder contents
|
||||||
url = `/api/folders/${app.currentPath}/contents?t=${timestamp}`;
|
url = `/api/folders/${app.currentPath}/contents?t=${timestamp}`;
|
||||||
console.log(`Cargando contenido de subcarpeta: ${app.currentPath}`);
|
console.log(`Loading subfolder content: ${app.currentPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('oxicloud_token');
|
const token = localStorage.getItem('oxicloud_token');
|
||||||
@@ -686,14 +686,14 @@ async function loadFiles(options = {}) {
|
|||||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
'Pragma': 'no-cache'
|
'Pragma': 'no-cache'
|
||||||
},
|
},
|
||||||
cache: 'no-store' // Instruir al navegador a no usar caché
|
cache: 'no-store' // Instruct the browser not to use cache
|
||||||
};
|
};
|
||||||
|
|
||||||
// Si se especifica forceRefresh, agregar un parámetro adicional para evitar caché
|
// If forceRefresh is specified, add an additional parameter to avoid cache
|
||||||
if (forceRefresh) {
|
if (forceRefresh) {
|
||||||
url += `&force_refresh=true`;
|
url += `&force_refresh=true`;
|
||||||
requestOptions.headers['X-Force-Refresh'] = 'true';
|
requestOptions.headers['X-Force-Refresh'] = 'true';
|
||||||
console.log('Forzando refresco completo ignorando caché');
|
console.log('Forcing complete refresh ignoring cache');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Loading files from ${url}`);
|
console.log(`Loading files from ${url}`);
|
||||||
@@ -703,13 +703,13 @@ async function loadFiles(options = {}) {
|
|||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
console.warn("Auth error when loading files, showing empty list");
|
console.warn("Auth error when loading files, showing empty list");
|
||||||
// Just show empty state instead of causing redirect loops
|
// Just show empty state instead of causing redirect loops
|
||||||
elements.filesGrid.innerHTML = '<div class="empty-state"><p>No se pudieron cargar los archivos</p></div>';
|
elements.filesGrid.innerHTML = '<div class="empty-state"><p>Could not load files</p></div>';
|
||||||
elements.filesListView.innerHTML = `
|
elements.filesListView.innerHTML = `
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div>Nombre</div>
|
<div>Name</div>
|
||||||
<div>Tipo</div>
|
<div>Type</div>
|
||||||
<div>Tamaño</div>
|
<div>Size</div>
|
||||||
<div>Modificado</div>
|
<div>Modified</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -724,10 +724,10 @@ async function loadFiles(options = {}) {
|
|||||||
elements.filesGrid.innerHTML = '';
|
elements.filesGrid.innerHTML = '';
|
||||||
elements.filesListView.innerHTML = `
|
elements.filesListView.innerHTML = `
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div data-i18n="files.name">Nombre</div>
|
<div data-i18n="files.name">Name</div>
|
||||||
<div data-i18n="files.type">Tipo</div>
|
<div data-i18n="files.type">Type</div>
|
||||||
<div data-i18n="files.size">Tamaño</div>
|
<div data-i18n="files.size">Size</div>
|
||||||
<div data-i18n="files.modified">Modificado</div>
|
<div data-i18n="files.modified">Modified</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -766,11 +766,11 @@ async function loadFiles(options = {}) {
|
|||||||
|
|
||||||
// Also load files in this folder
|
// Also load files in this folder
|
||||||
const cacheTimestamp = new Date().getTime();
|
const cacheTimestamp = new Date().getTime();
|
||||||
let filesUrl = `/api/files?t=${cacheTimestamp}`; // Agregar timestamp para evitar problemas de caché
|
let filesUrl = `/api/files?t=${cacheTimestamp}`; // Add timestamp to avoid cache issues
|
||||||
if (app.currentPath) {
|
if (app.currentPath) {
|
||||||
filesUrl += `&folder_id=${app.currentPath}`;
|
filesUrl += `&folder_id=${app.currentPath}`;
|
||||||
}
|
}
|
||||||
console.log(`Cargando archivos desde: ${filesUrl}`);
|
console.log(`Loading files from: ${filesUrl}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`Fetching files from: ${filesUrl}`);
|
console.log(`Fetching files from: ${filesUrl}`);
|
||||||
@@ -810,7 +810,7 @@ async function loadFiles(options = {}) {
|
|||||||
console.error('Error loading folders:', error);
|
console.error('Error loading folders:', error);
|
||||||
ui.showNotification('Error', 'Could not load files and folders');
|
ui.showNotification('Error', 'Could not load files and folders');
|
||||||
} finally {
|
} finally {
|
||||||
// Marcar que ya no estamos cargando archivos para permitir solicitudes futuras
|
// Mark that we are no longer loading files to allow future requests
|
||||||
window.isLoadingFiles = false;
|
window.isLoadingFiles = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -839,11 +839,11 @@ async function loadTrashItems() {
|
|||||||
elements.filesGrid.innerHTML = '';
|
elements.filesGrid.innerHTML = '';
|
||||||
elements.filesListView.innerHTML = `
|
elements.filesListView.innerHTML = `
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div data-i18n="files.name">Nombre</div>
|
<div data-i18n="files.name">Name</div>
|
||||||
<div data-i18n="files.type">Tipo</div>
|
<div data-i18n="files.type">Type</div>
|
||||||
<div data-i18n="trash.original_location">Ubicación original</div>
|
<div data-i18n="trash.original_location">Original location</div>
|
||||||
<div data-i18n="trash.deleted_date">Fecha eliminación</div>
|
<div data-i18n="trash.deleted_date">Deletion date</div>
|
||||||
<div data-i18n="trash.actions">Acciones</div>
|
<div data-i18n="trash.actions">Actions</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -864,7 +864,7 @@ async function loadTrashItems() {
|
|||||||
emptyState.className = 'empty-state';
|
emptyState.className = 'empty-state';
|
||||||
emptyState.innerHTML = `
|
emptyState.innerHTML = `
|
||||||
<i class="fas fa-trash" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
<i class="fas fa-trash" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
||||||
<p>${window.i18n ? window.i18n.t('trash.empty_state') : 'La papelera está vacía'}</p>
|
<p>${window.i18n ? window.i18n.t('trash.empty_state') : 'The trash is empty'}</p>
|
||||||
`;
|
`;
|
||||||
elements.filesGrid.appendChild(emptyState);
|
elements.filesGrid.appendChild(emptyState);
|
||||||
return;
|
return;
|
||||||
@@ -877,7 +877,7 @@ async function loadTrashItems() {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading trash items:', error);
|
console.error('Error loading trash items:', error);
|
||||||
window.ui.showNotification('Error', 'Error al cargar elementos de la papelera');
|
window.ui.showNotification('Error', 'Error loading trash items');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,8 +896,8 @@ function addTrashItemToView(item) {
|
|||||||
|
|
||||||
// Item type label
|
// Item type label
|
||||||
const typeLabel = isFile ?
|
const typeLabel = isFile ?
|
||||||
(window.i18n ? window.i18n.t('files.file_types.file') : 'Archivo') :
|
(window.i18n ? window.i18n.t('files.file_types.file') : 'File') :
|
||||||
(window.i18n ? window.i18n.t('files.file_types.folder') : 'Carpeta');
|
(window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder');
|
||||||
|
|
||||||
// Grid view element
|
// Grid view element
|
||||||
const gridElement = document.createElement('div');
|
const gridElement = document.createElement('div');
|
||||||
@@ -912,10 +912,10 @@ function addTrashItemToView(item) {
|
|||||||
<div class="file-name">${item.name}</div>
|
<div class="file-name">${item.name}</div>
|
||||||
<div class="file-info">${typeLabel} - ${formattedDate}</div>
|
<div class="file-info">${typeLabel} - ${formattedDate}</div>
|
||||||
<div class="trash-actions">
|
<div class="trash-actions">
|
||||||
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restaurar'}">
|
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restore'}">
|
||||||
<i class="fas fa-undo"></i>
|
<i class="fas fa-undo"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Eliminar permanentemente'}">
|
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Delete permanently'}">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -956,10 +956,10 @@ function addTrashItemToView(item) {
|
|||||||
<div class="path-cell">${item.original_path || '--'}</div>
|
<div class="path-cell">${item.original_path || '--'}</div>
|
||||||
<div class="date-cell">${formattedDate}</div>
|
<div class="date-cell">${formattedDate}</div>
|
||||||
<div class="actions-cell">
|
<div class="actions-cell">
|
||||||
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restaurar'}">
|
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restore'}">
|
||||||
<i class="fas fa-undo"></i>
|
<i class="fas fa-undo"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Eliminar permanentemente'}">
|
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Delete permanently'}">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -995,7 +995,7 @@ async function performSearch(query) {
|
|||||||
app.isSearchMode = true;
|
app.isSearchMode = true;
|
||||||
|
|
||||||
// Set breadcrumb for search
|
// Set breadcrumb for search
|
||||||
ui.updateBreadcrumb(`Búsqueda: "${query}"`);
|
ui.updateBreadcrumb(`Search: "${query}"`);
|
||||||
|
|
||||||
// Prepare search options
|
// Prepare search options
|
||||||
const options = {
|
const options = {
|
||||||
@@ -1037,7 +1037,7 @@ async function performSearch(query) {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search error:', error);
|
console.error('Search error:', error);
|
||||||
window.ui.showNotification('Error', 'Error al realizar la búsqueda');
|
window.ui.showNotification('Error', 'Error performing search');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,7 +1076,7 @@ function switchToSharedView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.shared') : 'Compartidos';
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.shared') : 'Shared';
|
||||||
|
|
||||||
// Clear breadcrumb and show root
|
// Clear breadcrumb and show root
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
@@ -1105,7 +1105,7 @@ function switchToFilesView() {
|
|||||||
app.currentSection = 'files';
|
app.currentSection = 'files';
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Archivos';
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
|
||||||
|
|
||||||
// Remove active class from all nav items
|
// Remove active class from all nav items
|
||||||
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
|
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
|
||||||
@@ -1122,29 +1122,29 @@ function switchToFilesView() {
|
|||||||
<div class="upload-dropdown" id="upload-dropdown">
|
<div class="upload-dropdown" id="upload-dropdown">
|
||||||
<button class="btn btn-primary" id="upload-btn">
|
<button class="btn btn-primary" id="upload-btn">
|
||||||
<i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i>
|
<i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i>
|
||||||
<span data-i18n="actions.upload">Subir</span>
|
<span data-i18n="actions.upload">Upload</span>
|
||||||
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
|
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
|
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
|
||||||
<button class="upload-dropdown-item" id="upload-files-btn">
|
<button class="upload-dropdown-item" id="upload-files-btn">
|
||||||
<i class="fas fa-file"></i>
|
<i class="fas fa-file"></i>
|
||||||
<span data-i18n="actions.upload_files">Subir archivos</span>
|
<span data-i18n="actions.upload_files">Upload files</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="upload-dropdown-item" id="upload-folder-btn">
|
<button class="upload-dropdown-item" id="upload-folder-btn">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
<span data-i18n="actions.upload_folder">Subir carpeta</span>
|
<span data-i18n="actions.upload_folder">Upload folder</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary" id="new-folder-btn">
|
<button class="btn btn-secondary" id="new-folder-btn">
|
||||||
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">Nueva carpeta</span>
|
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">New folder</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
<button class="toggle-btn active" id="grid-view-btn" title="Vista de cuadrícula">
|
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
||||||
<i class="fas fa-th"></i>
|
<i class="fas fa-th"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="toggle-btn" id="list-view-btn" title="Vista de lista">
|
<button class="toggle-btn" id="list-view-btn" title="List view">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1219,7 +1219,7 @@ function switchToFavoritesView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.favorites') : 'Favoritos';
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.favorites') : 'Favorites';
|
||||||
|
|
||||||
// Clear breadcrumb and show root
|
// Clear breadcrumb and show root
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
@@ -1235,10 +1235,10 @@ function switchToFavoritesView() {
|
|||||||
<!-- No actions needed for favorites view -->
|
<!-- No actions needed for favorites view -->
|
||||||
</div>
|
</div>
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
<button class="toggle-btn active" id="grid-view-btn" title="Vista de cuadrícula">
|
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
||||||
<i class="fas fa-th"></i>
|
<i class="fas fa-th"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="toggle-btn" id="list-view-btn" title="Vista de lista">
|
<button class="toggle-btn" id="list-view-btn" title="List view">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1278,7 +1278,7 @@ function switchToFavoritesView() {
|
|||||||
filesGrid.innerHTML = `
|
filesGrid.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i>
|
<i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i>
|
||||||
<p>Error al cargar el módulo de favoritos</p>
|
<p>Error loading the favorites module</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -1308,7 +1308,7 @@ function switchToRecentFilesView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.recent') : 'Recientes';
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.recent') : 'Recent';
|
||||||
|
|
||||||
// Clear breadcrumb and show root
|
// Clear breadcrumb and show root
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb('');
|
||||||
@@ -1322,14 +1322,14 @@ function switchToRecentFilesView() {
|
|||||||
elements.actionsBar.innerHTML = `
|
elements.actionsBar.innerHTML = `
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="btn btn-secondary" id="clear-recent-btn">
|
<button class="btn btn-secondary" id="clear-recent-btn">
|
||||||
<i class="fas fa-broom" style="margin-right: 5px;"></i> <span data-i18n="actions.clear_recent">Limpiar recientes</span>
|
<i class="fas fa-broom" style="margin-right: 5px;"></i> <span data-i18n="actions.clear_recent">Clear recent</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
<button class="toggle-btn active" id="grid-view-btn" title="Vista de cuadrícula">
|
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
||||||
<i class="fas fa-th"></i>
|
<i class="fas fa-th"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="toggle-btn" id="list-view-btn" title="Vista de lista">
|
<button class="toggle-btn" id="list-view-btn" title="List view">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1341,7 +1341,7 @@ function switchToRecentFilesView() {
|
|||||||
if (window.recent) {
|
if (window.recent) {
|
||||||
window.recent.clearRecentFiles();
|
window.recent.clearRecentFiles();
|
||||||
window.recent.displayRecentFiles();
|
window.recent.displayRecentFiles();
|
||||||
window.ui.showNotification('Limpieza completada', 'Se ha limpiado el historial de archivos recientes');
|
window.ui.showNotification('Cleanup completed', 'Recent files history has been cleared');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1378,7 +1378,7 @@ function switchToRecentFilesView() {
|
|||||||
filesGrid.innerHTML = `
|
filesGrid.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i>
|
<i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i>
|
||||||
<p>Error al cargar el módulo de archivos recientes</p>
|
<p>Error loading the recent files module</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -1450,7 +1450,7 @@ window.refreshUserData = refreshUserData;
|
|||||||
function showUserProfileModal() {
|
function showUserProfileModal() {
|
||||||
const USER_DATA_KEY = 'oxicloud_user';
|
const USER_DATA_KEY = 'oxicloud_user';
|
||||||
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
||||||
const username = userData.username || 'Usuario';
|
const username = userData.username || 'User';
|
||||||
const email = userData.email || '';
|
const email = userData.email || '';
|
||||||
const role = userData.role || 'user';
|
const role = userData.role || 'user';
|
||||||
const initials = username.substring(0, 2).toUpperCase();
|
const initials = username.substring(0, 2).toUpperCase();
|
||||||
@@ -1847,7 +1847,7 @@ async function findUserHomeFolder(username) {
|
|||||||
* Logout - clear all auth data and redirect to login
|
* Logout - clear all auth data and redirect to login
|
||||||
*/
|
*/
|
||||||
function logout() {
|
function logout() {
|
||||||
// Nombres de variables según auth.js
|
// Variable names as per auth.js
|
||||||
const TOKEN_KEY = 'oxicloud_token';
|
const TOKEN_KEY = 'oxicloud_token';
|
||||||
const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token';
|
const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token';
|
||||||
const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
|
const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
|
||||||
|
|||||||
+20
-20
@@ -34,7 +34,7 @@ const LANGUAGE_TEXTS = {
|
|||||||
subtitle: 'Por favor, selecciona tu idioma',
|
subtitle: 'Por favor, selecciona tu idioma',
|
||||||
continue: 'Continuar',
|
continue: 'Continuar',
|
||||||
autodetected: 'Hemos detectado tu idioma',
|
autodetected: 'Hemos detectado tu idioma',
|
||||||
moreLanguages: 'Más idiomas...',
|
moreLanguages: 'More languages...',
|
||||||
modalTitle: 'Seleccionar idioma',
|
modalTitle: 'Seleccionar idioma',
|
||||||
searchPlaceholder: 'Buscar idioma...'
|
searchPlaceholder: 'Buscar idioma...'
|
||||||
},
|
},
|
||||||
@@ -494,8 +494,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
console.error('Error showing initial panel:', err);
|
console.error('Error showing initial panel:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Siempre limpiar los contadores al cargar la página de login
|
// Always clear counters when loading the login page
|
||||||
// para asegurar que no quedamos atrapados en un bucle
|
// to ensure we don't get trapped in a loop
|
||||||
console.log('Login page loaded, clearing all counters');
|
console.log('Login page loaded, clearing all counters');
|
||||||
sessionStorage.removeItem('redirect_count');
|
sessionStorage.removeItem('redirect_count');
|
||||||
localStorage.removeItem('refresh_attempts');
|
localStorage.removeItem('refresh_attempts');
|
||||||
@@ -589,14 +589,14 @@ if (isLoginPage && loginForm) {
|
|||||||
localStorage.setItem(TOKEN_KEY, token);
|
localStorage.setItem(TOKEN_KEY, token);
|
||||||
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
||||||
|
|
||||||
// Extraer fecha de expiración desde el token JWT
|
// Extract expiration date from the JWT token
|
||||||
let parsedExpiry = false;
|
let parsedExpiry = false;
|
||||||
const tokenParts = token.split('.');
|
const tokenParts = token.split('.');
|
||||||
if (tokenParts.length === 3) {
|
if (tokenParts.length === 3) {
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(atob(tokenParts[1]));
|
const payload = JSON.parse(atob(tokenParts[1]));
|
||||||
if (payload.exp) {
|
if (payload.exp) {
|
||||||
// payload.exp está en segundos desde epoch
|
// payload.exp is in seconds since epoch
|
||||||
const expiryDate = new Date(payload.exp * 1000);
|
const expiryDate = new Date(payload.exp * 1000);
|
||||||
|
|
||||||
// Verify the date is valid
|
// Verify the date is valid
|
||||||
@@ -642,7 +642,7 @@ if (isLoginPage && loginForm) {
|
|||||||
// Redirect to main app
|
// Redirect to main app
|
||||||
redirectToMainApp();
|
redirectToMainApp();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loginError.textContent = error.message || 'Error al iniciar sesión';
|
loginError.textContent = error.message || 'Error logging in';
|
||||||
loginError.style.display = 'block';
|
loginError.style.display = 'block';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -664,7 +664,7 @@ if (isLoginPage && registerForm) {
|
|||||||
|
|
||||||
// Validate passwords match
|
// Validate passwords match
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Las contraseñas no coinciden';
|
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
||||||
registerError.textContent = errorMsg;
|
registerError.textContent = errorMsg;
|
||||||
registerError.style.display = 'block';
|
registerError.style.display = 'block';
|
||||||
return;
|
return;
|
||||||
@@ -674,7 +674,7 @@ if (isLoginPage && registerForm) {
|
|||||||
const data = await register(username, email, password);
|
const data = await register(username, email, password);
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
const successMsg = window.i18n ? window.i18n.t('auth.account_success') : '¡Cuenta creada con éxito! Puedes iniciar sesión ahora.';
|
const successMsg = window.i18n ? window.i18n.t('auth.account_success') : 'Account created successfully! You can now log in.';
|
||||||
registerSuccess.textContent = successMsg;
|
registerSuccess.textContent = successMsg;
|
||||||
registerSuccess.style.display = 'block';
|
registerSuccess.style.display = 'block';
|
||||||
|
|
||||||
@@ -687,7 +687,7 @@ if (isLoginPage && registerForm) {
|
|||||||
registerPanel.style.display = 'none';
|
registerPanel.style.display = 'none';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error al registrar cuenta';
|
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error registering account';
|
||||||
registerError.textContent = error.message || errorMsg;
|
registerError.textContent = error.message || errorMsg;
|
||||||
registerError.style.display = 'block';
|
registerError.style.display = 'block';
|
||||||
}
|
}
|
||||||
@@ -710,7 +710,7 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
|
|
||||||
// Validate passwords match
|
// Validate passwords match
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Las contraseñas no coinciden';
|
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
||||||
adminSetupError.textContent = errorMsg;
|
adminSetupError.textContent = errorMsg;
|
||||||
adminSetupError.style.display = 'block';
|
adminSetupError.style.display = 'block';
|
||||||
return;
|
return;
|
||||||
@@ -721,7 +721,7 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
const data = await register('admin', email, password, 'admin');
|
const data = await register('admin', email, password, 'admin');
|
||||||
|
|
||||||
// Show success message in the GUI instead of alert
|
// Show success message in the GUI instead of alert
|
||||||
const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : '¡Cuenta de administrador creada con éxito! Ahora puedes iniciar sesión.';
|
const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.';
|
||||||
|
|
||||||
if (adminSetupSuccess) {
|
if (adminSetupSuccess) {
|
||||||
adminSetupSuccess.textContent = successMsg;
|
adminSetupSuccess.textContent = successMsg;
|
||||||
@@ -736,7 +736,7 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error al crear cuenta de administrador';
|
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error creating admin account';
|
||||||
adminSetupError.textContent = error.message || errorMsg;
|
adminSetupError.textContent = error.message || errorMsg;
|
||||||
adminSetupError.style.display = 'block';
|
adminSetupError.style.display = 'block';
|
||||||
}
|
}
|
||||||
@@ -792,10 +792,10 @@ async function login(username, password) {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(errorData.error || 'Falló la autenticación');
|
throw new Error(errorData.error || 'Authentication failed');
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
// If the error response is not valid JSON
|
// If the error response is not valid JSON
|
||||||
throw new Error(`Error de autenticación (${response.status}): ${response.statusText}`);
|
throw new Error(`Authentication error (${response.status}): ${response.statusText}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,7 +806,7 @@ async function login(username, password) {
|
|||||||
return data;
|
return data;
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
console.error('Error parsing login response:', jsonError);
|
console.error('Error parsing login response:', jsonError);
|
||||||
throw new Error('Error al procesar la respuesta del servidor');
|
throw new Error('Error processing server response');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
@@ -848,10 +848,10 @@ async function register(username, email, password, role = 'user') {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(errorData.error || 'Error en el registro');
|
throw new Error(errorData.error || 'Registration error');
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
// If the error response is not valid JSON
|
// If the error response is not valid JSON
|
||||||
throw new Error(`Error de registro (${response.status}): ${response.statusText}`);
|
throw new Error(`Registration error (${response.status}): ${response.statusText}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,7 +862,7 @@ async function register(username, email, password, role = 'user') {
|
|||||||
return data;
|
return data;
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
console.error('Error parsing registration response:', jsonError);
|
console.error('Error parsing registration response:', jsonError);
|
||||||
throw new Error('Error al procesar la respuesta del servidor');
|
throw new Error('Error processing server response');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Registration error:', error);
|
console.error('Registration error:', error);
|
||||||
@@ -883,7 +883,7 @@ async function fetchUserData(token) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Error al obtener datos del usuario');
|
throw new Error('Error fetching user data');
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
@@ -1092,7 +1092,7 @@ function redirectToMainApp() {
|
|||||||
window.location.href = '/login.html?error=redirect_fatal';
|
window.location.href = '/login.html?error=redirect_fatal';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Nothing more we can do
|
// Nothing more we can do
|
||||||
alert('Error crítico en la redirección. Por favor, recarga la página e intenta nuevamente.');
|
alert('Critical redirect error. Please reload the page and try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-26
@@ -30,7 +30,7 @@ const contextMenus = {
|
|||||||
window.favorites.removeFromFavorites(folder.id, 'folder');
|
window.favorites.removeFromFavorites(folder.id, 'folder');
|
||||||
// Update menu item text
|
// Update menu item text
|
||||||
document.getElementById('favorite-folder-option').querySelector('span').textContent =
|
document.getElementById('favorite-folder-option').querySelector('span').textContent =
|
||||||
window.i18n ? window.i18n.t('actions.favorite') : 'Añadir a favoritos';
|
window.i18n ? window.i18n.t('actions.favorite') : 'Add to favorites';
|
||||||
} else {
|
} else {
|
||||||
// Add to favorites
|
// Add to favorites
|
||||||
window.favorites.addToFavorites(
|
window.favorites.addToFavorites(
|
||||||
@@ -41,7 +41,7 @@ const contextMenus = {
|
|||||||
);
|
);
|
||||||
// Update menu item text
|
// Update menu item text
|
||||||
document.getElementById('favorite-folder-option').querySelector('span').textContent =
|
document.getElementById('favorite-folder-option').querySelector('span').textContent =
|
||||||
window.i18n ? window.i18n.t('actions.unfavorite') : 'Quitar de favoritos';
|
window.i18n ? window.i18n.t('actions.unfavorite') : 'Remove from favorites';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.ui.closeContextMenu();
|
window.ui.closeContextMenu();
|
||||||
@@ -143,7 +143,7 @@ const contextMenus = {
|
|||||||
window.favorites.removeFromFavorites(file.id, 'file');
|
window.favorites.removeFromFavorites(file.id, 'file');
|
||||||
// Update menu item text
|
// Update menu item text
|
||||||
document.getElementById('favorite-file-option').querySelector('span').textContent =
|
document.getElementById('favorite-file-option').querySelector('span').textContent =
|
||||||
window.i18n ? window.i18n.t('actions.favorite') : 'Añadir a favoritos';
|
window.i18n ? window.i18n.t('actions.favorite') : 'Add to favorites';
|
||||||
} else {
|
} else {
|
||||||
// Add to favorites
|
// Add to favorites
|
||||||
window.favorites.addToFavorites(
|
window.favorites.addToFavorites(
|
||||||
@@ -154,7 +154,7 @@ const contextMenus = {
|
|||||||
);
|
);
|
||||||
// Update menu item text
|
// Update menu item text
|
||||||
document.getElementById('favorite-file-option').querySelector('span').textContent =
|
document.getElementById('favorite-file-option').querySelector('span').textContent =
|
||||||
window.i18n ? window.i18n.t('actions.unfavorite') : 'Quitar de favoritos';
|
window.i18n ? window.i18n.t('actions.unfavorite') : 'Remove from favorites';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.ui.closeFileContextMenu();
|
window.ui.closeFileContextMenu();
|
||||||
@@ -246,7 +246,7 @@ const contextMenus = {
|
|||||||
renameInput.value = folder.name;
|
renameInput.value = folder.name;
|
||||||
// Update header text
|
// Update header text
|
||||||
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
|
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
|
||||||
if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_folder') : 'Renombrar carpeta';
|
if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_folder') : 'Rename folder';
|
||||||
renameDialog.style.display = 'flex';
|
renameDialog.style.display = 'flex';
|
||||||
renameInput.focus();
|
renameInput.focus();
|
||||||
renameInput.select();
|
renameInput.select();
|
||||||
@@ -264,7 +264,7 @@ const contextMenus = {
|
|||||||
renameInput.value = file.name;
|
renameInput.value = file.name;
|
||||||
// Update header text
|
// Update header text
|
||||||
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
|
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
|
||||||
if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_file') : 'Renombrar archivo';
|
if (headerSpan) headerSpan.textContent = window.i18n ? window.i18n.t('dialogs.rename_file') : 'Rename file';
|
||||||
renameDialog.style.display = 'flex';
|
renameDialog.style.display = 'flex';
|
||||||
renameInput.focus();
|
renameInput.focus();
|
||||||
renameInput.select();
|
renameInput.select();
|
||||||
@@ -293,8 +293,8 @@ const contextMenus = {
|
|||||||
// Update dialog title (preserve icon)
|
// Update dialog title (preserve icon)
|
||||||
const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header');
|
const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header');
|
||||||
const titleText = mode === 'file' ?
|
const titleText = mode === 'file' ?
|
||||||
(window.i18n ? window.i18n.t('dialogs.move_file') : 'Mover archivo') :
|
(window.i18n ? window.i18n.t('dialogs.move_file') : 'Move file') :
|
||||||
(window.i18n ? window.i18n.t('dialogs.move_folder') : 'Mover carpeta');
|
(window.i18n ? window.i18n.t('dialogs.move_folder') : 'Move folder');
|
||||||
dialogHeader.innerHTML = `<i class="fas fa-arrows-alt" style="color:#ff5e3a"></i> <span>${titleText}</span>`;
|
dialogHeader.innerHTML = `<i class="fas fa-arrows-alt" style="color:#ff5e3a"></i> <span>${titleText}</span>`;
|
||||||
|
|
||||||
// Load all available folders
|
// Load all available folders
|
||||||
@@ -319,7 +319,7 @@ const contextMenus = {
|
|||||||
async renameItem() {
|
async renameItem() {
|
||||||
const newName = document.getElementById('rename-input').value.trim();
|
const newName = document.getElementById('rename-input').value.trim();
|
||||||
if (!newName) {
|
if (!newName) {
|
||||||
alert(window.i18n ? window.i18n.t('errors.empty_name') : 'El nombre no puede estar vacío');
|
alert(window.i18n ? window.i18n.t('errors.empty_name') : 'Name cannot be empty');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ const contextMenus = {
|
|||||||
// Clear container except root option
|
// Clear container except root option
|
||||||
folderSelectContainer.innerHTML = `
|
folderSelectContainer.innerHTML = `
|
||||||
<div class="folder-select-item selected" data-folder-id="">
|
<div class="folder-select-item selected" data-folder-id="">
|
||||||
<i class="fas fa-folder"></i> <span data-i18n="dialogs.root">Raíz</span>
|
<i class="fas fa-folder"></i> <span data-i18n="dialogs.root">Root</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -438,8 +438,8 @@ const contextMenus = {
|
|||||||
|
|
||||||
// Update dialog content
|
// Update dialog content
|
||||||
dialogHeader.textContent = itemType === 'file' ?
|
dialogHeader.textContent = itemType === 'file' ?
|
||||||
(window.i18n ? window.i18n.t('dialogs.share_file') : 'Compartir archivo') :
|
(window.i18n ? window.i18n.t('dialogs.share_file') : 'Share file') :
|
||||||
(window.i18n ? window.i18n.t('dialogs.share_folder') : 'Compartir carpeta');
|
(window.i18n ? window.i18n.t('dialogs.share_folder') : 'Share folder');
|
||||||
|
|
||||||
itemName.textContent = item.name;
|
itemName.textContent = item.name;
|
||||||
|
|
||||||
@@ -470,21 +470,21 @@ const contextMenus = {
|
|||||||
shareEl.className = 'existing-share-item';
|
shareEl.className = 'existing-share-item';
|
||||||
|
|
||||||
const expiresText = share.expires_at ?
|
const expiresText = share.expires_at ?
|
||||||
`Vence: ${window.fileSharing.formatExpirationDate(share.expires_at)}` :
|
`Expires: ${window.fileSharing.formatExpirationDate(share.expires_at)}` :
|
||||||
'Sin vencimiento';
|
'No expiration';
|
||||||
|
|
||||||
shareEl.innerHTML = `
|
shareEl.innerHTML = `
|
||||||
<div class="share-url">${share.url}</div>
|
<div class="share-url">${share.url}</div>
|
||||||
<div class="share-info">
|
<div class="share-info">
|
||||||
${share.password_protected ? '<span class="share-protected"><i class="fas fa-lock"></i> Con contraseña</span>' : ''}
|
${share.password_protected ? '<span class="share-protected"><i class="fas fa-lock"></i> Password protected</span>' : ''}
|
||||||
<span class="share-expiration">${expiresText}</span>
|
<span class="share-expiration">${expiresText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="share-actions">
|
<div class="share-actions">
|
||||||
<button class="btn btn-small copy-link-btn" data-share-url="${share.url}">
|
<button class="btn btn-small copy-link-btn" data-share-url="${share.url}">
|
||||||
<i class="fas fa-copy"></i> Copiar
|
<i class="fas fa-copy"></i> Copy
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-small btn-danger delete-link-btn" data-share-id="${share.id}">
|
<button class="btn btn-small btn-danger delete-link-btn" data-share-id="${share.id}">
|
||||||
<i class="fas fa-trash"></i> Eliminar
|
<i class="fas fa-trash"></i> Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -507,9 +507,9 @@ const contextMenus = {
|
|||||||
const shareId = btn.getAttribute('data-share-id');
|
const shareId = btn.getAttribute('data-share-id');
|
||||||
|
|
||||||
showConfirmDialog({
|
showConfirmDialog({
|
||||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete_share') : 'Eliminar enlace',
|
title: window.i18n ? window.i18n.t('dialogs.confirm_delete_share') : 'Delete link',
|
||||||
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_share_msg') : '¿Estás seguro de que quieres eliminar este enlace compartido?',
|
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_share_msg') : 'Are you sure you want to delete this shared link?',
|
||||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Eliminar',
|
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
|
||||||
}).then(confirmed => {
|
}).then(confirmed => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
window.fileSharing.removeSharedLink(shareId);
|
window.fileSharing.removeSharedLink(shareId);
|
||||||
@@ -534,7 +534,7 @@ const contextMenus = {
|
|||||||
*/
|
*/
|
||||||
createSharedLink() {
|
createSharedLink() {
|
||||||
if (!window.app.shareDialogItem || !window.app.shareDialogItemType) {
|
if (!window.app.shareDialogItem || !window.app.shareDialogItemType) {
|
||||||
window.ui.showNotification('Error', 'No se pudo compartir el elemento');
|
window.ui.showNotification('Error', 'Could not share the item');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,14 +577,14 @@ const contextMenus = {
|
|||||||
shareUrl.select();
|
shareUrl.select();
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
window.ui.showNotification('Enlace creado', 'Enlace compartido creado correctamente');
|
window.ui.showNotification('Link created', 'Shared link created successfully');
|
||||||
|
|
||||||
// Reload existing shares
|
// Reload existing shares
|
||||||
this.showShareDialog(item, itemType);
|
this.showShareDialog(item, itemType);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating shared link:', error);
|
console.error('Error creating shared link:', error);
|
||||||
window.ui.showNotification('Error', 'No se pudo crear el enlace compartido');
|
window.ui.showNotification('Error', 'Could not create shared link');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -614,14 +614,14 @@ const contextMenus = {
|
|||||||
const shareUrl = window.app.notificationShareUrl;
|
const shareUrl = window.app.notificationShareUrl;
|
||||||
|
|
||||||
if (!email || !shareUrl) {
|
if (!email || !shareUrl) {
|
||||||
window.ui.showNotification('Error', 'Por favor, ingresa un correo electrónico válido');
|
window.ui.showNotification('Error', 'Please enter a valid email address');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate email format
|
// Validate email format
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
if (!emailRegex.test(email)) {
|
if (!emailRegex.test(email)) {
|
||||||
window.ui.showNotification('Error', 'Por favor, ingresa un correo electrónico válido');
|
window.ui.showNotification('Error', 'Please enter a valid email address');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,7 +630,7 @@ const contextMenus = {
|
|||||||
document.getElementById('notification-dialog').style.display = 'none';
|
document.getElementById('notification-dialog').style.display = 'none';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending notification:', error);
|
console.error('Error sending notification:', error);
|
||||||
window.ui.showNotification('Error', 'No se pudo enviar la notificación');
|
window.ui.showNotification('Error', 'Could not send notification');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+17
-17
@@ -230,8 +230,8 @@ const favorites = {
|
|||||||
|
|
||||||
// Show success notification
|
// Show success notification
|
||||||
window.ui.showNotification(
|
window.ui.showNotification(
|
||||||
'Añadido a favoritos',
|
'Added to favorites',
|
||||||
`"${name}" añadido a favoritos`
|
`"${name}" added to favorites`
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -271,8 +271,8 @@ const favorites = {
|
|||||||
// Show success notification if item was found
|
// Show success notification if item was found
|
||||||
if (item) {
|
if (item) {
|
||||||
window.ui.showNotification(
|
window.ui.showNotification(
|
||||||
'Eliminado de favoritos',
|
'Removed from favorites',
|
||||||
`"${item.name}" eliminado de favoritos`
|
`"${item.name}" removed from favorites`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -310,10 +310,10 @@ const favorites = {
|
|||||||
filesGrid.innerHTML = '';
|
filesGrid.innerHTML = '';
|
||||||
filesListView.innerHTML = `
|
filesListView.innerHTML = `
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div data-i18n="files.name">Nombre</div>
|
<div data-i18n="files.name">Name</div>
|
||||||
<div data-i18n="files.type">Tipo</div>
|
<div data-i18n="files.type">Type</div>
|
||||||
<div data-i18n="files.size">Tamaño</div>
|
<div data-i18n="files.size">Size</div>
|
||||||
<div data-i18n="files.modified">Modificado</div>
|
<div data-i18n="files.modified">Modified</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -326,8 +326,8 @@ const favorites = {
|
|||||||
emptyState.className = 'empty-state';
|
emptyState.className = 'empty-state';
|
||||||
emptyState.innerHTML = `
|
emptyState.innerHTML = `
|
||||||
<i class="fas fa-star" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
<i class="fas fa-star" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
||||||
<p>${window.i18n ? window.i18n.t('favorites.empty_state') : 'No hay elementos favoritos'}</p>
|
<p>${window.i18n ? window.i18n.t('favorites.empty_state') : 'No favorite items'}</p>
|
||||||
<p>${window.i18n ? window.i18n.t('favorites.empty_hint') : 'Para marcar como favorito, haz clic derecho en cualquier archivo o carpeta'}</p>
|
<p>${window.i18n ? window.i18n.t('favorites.empty_hint') : 'To mark as favorite, right-click on any file or folder'}</p>
|
||||||
`;
|
`;
|
||||||
filesGrid.appendChild(emptyState);
|
filesGrid.appendChild(emptyState);
|
||||||
return;
|
return;
|
||||||
@@ -359,7 +359,7 @@ const favorites = {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error displaying favorites:', error);
|
console.error('Error displaying favorites:', error);
|
||||||
window.ui.showNotification('Error', 'Error al cargar elementos favoritos');
|
window.ui.showNotification('Error', 'Error loading favorite items');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -468,7 +468,7 @@ const favorites = {
|
|||||||
<i class="fas fa-folder"></i>
|
<i class="fas fa-folder"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-name">${folder.name}</div>
|
<div class="file-name">${folder.name}</div>
|
||||||
<div class="file-info">Carpeta</div>
|
<div class="file-info">Folder</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Click to navigate
|
// Click to navigate
|
||||||
@@ -518,7 +518,7 @@ const favorites = {
|
|||||||
</div>
|
</div>
|
||||||
<span>${folder.name}</span>
|
<span>${folder.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Carpeta'}</div>
|
<div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}</div>
|
||||||
<div class="size-cell">--</div>
|
<div class="size-cell">--</div>
|
||||||
<div class="date-cell">${formattedDate}</div>
|
<div class="date-cell">${formattedDate}</div>
|
||||||
`;
|
`;
|
||||||
@@ -559,17 +559,17 @@ const favorites = {
|
|||||||
// Determine icon and type
|
// Determine icon and type
|
||||||
let iconClass = 'fas fa-file';
|
let iconClass = 'fas fa-file';
|
||||||
let iconSpecialClass = '';
|
let iconSpecialClass = '';
|
||||||
let typeLabel = 'Documento';
|
let typeLabel = 'Document';
|
||||||
|
|
||||||
if (file.mime_type) {
|
if (file.mime_type) {
|
||||||
if (file.mime_type.startsWith('image/')) {
|
if (file.mime_type.startsWith('image/')) {
|
||||||
iconClass = 'fas fa-file-image';
|
iconClass = 'fas fa-file-image';
|
||||||
iconSpecialClass = 'image-icon';
|
iconSpecialClass = 'image-icon';
|
||||||
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Imagen';
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Image';
|
||||||
} else if (file.mime_type.startsWith('text/')) {
|
} else if (file.mime_type.startsWith('text/')) {
|
||||||
iconClass = 'fas fa-file-alt';
|
iconClass = 'fas fa-file-alt';
|
||||||
iconSpecialClass = 'text-icon';
|
iconSpecialClass = 'text-icon';
|
||||||
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Texto';
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Text';
|
||||||
} else if (file.mime_type.startsWith('video/')) {
|
} else if (file.mime_type.startsWith('video/')) {
|
||||||
iconClass = 'fas fa-file-video';
|
iconClass = 'fas fa-file-video';
|
||||||
iconSpecialClass = 'video-icon';
|
iconSpecialClass = 'video-icon';
|
||||||
@@ -606,7 +606,7 @@ const favorites = {
|
|||||||
<i class="${iconClass}"></i>
|
<i class="${iconClass}"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-name">${file.name}</div>
|
<div class="file-name">${file.name}</div>
|
||||||
<div class="file-info">Modificado ${formattedDate.split(' ')[0]}</div>
|
<div class="file-info">Modified ${formattedDate.split(' ')[0]}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Download on click
|
// Download on click
|
||||||
|
|||||||
+91
-91
@@ -49,8 +49,8 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`);
|
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`);
|
||||||
|
|
||||||
// Usamos la URL correcta para la subida de archivos
|
// We use the correct URL for file upload
|
||||||
console.log('Formulario a enviar:', {
|
console.log('Form to submit:', {
|
||||||
file: file.name,
|
file: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
folder_id: targetFolderId || 'root'
|
folder_id: targetFolderId || 'root'
|
||||||
@@ -59,16 +59,16 @@ const fileOps = {
|
|||||||
const response = await fetch('/api/files/upload', {
|
const response = await fetch('/api/files/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
// Añadir cache: 'no-store' para evitar problemas de caché durante la subida
|
// Add cache: 'no-store' to avoid cache issues during upload
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
headers: {
|
headers: {
|
||||||
...getAuthHeaders(),
|
...getAuthHeaders(),
|
||||||
// Agregar este encabezado para forzar recargas frescas
|
// Add this header to force fresh reloads
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Respuesta del servidor:', {
|
console.log('Server response:', {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText
|
statusText: response.statusText
|
||||||
});
|
});
|
||||||
@@ -83,11 +83,11 @@ const fileOps = {
|
|||||||
console.log(`Successfully uploaded ${file.name}`, responseData);
|
console.log(`Successfully uploaded ${file.name}`, responseData);
|
||||||
|
|
||||||
// Show success notification immediately
|
// Show success notification immediately
|
||||||
window.ui.showNotification('Archivo subido', `${file.name} completado`);
|
window.ui.showNotification('File uploaded', `${file.name} completed`);
|
||||||
|
|
||||||
if (i === totalFiles - 1) {
|
if (i === totalFiles - 1) {
|
||||||
// Last file uploaded - wait and reload once
|
// Last file uploaded - wait and reload once
|
||||||
console.log('Último archivo subido, esperando antes de recargar...');
|
console.log('Last file uploaded, waiting before reloading...');
|
||||||
|
|
||||||
// Wait for backend to persist
|
// Wait for backend to persist
|
||||||
await new Promise(resolve => setTimeout(resolve, 800));
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
@@ -96,7 +96,7 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
await window.loadFiles({forceRefresh: true});
|
await window.loadFiles({forceRefresh: true});
|
||||||
} catch (reloadError) {
|
} catch (reloadError) {
|
||||||
console.error("Error recargando archivos:", reloadError);
|
console.error("Error reloading files:", reloadError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide upload UI
|
// Hide upload UI
|
||||||
@@ -109,11 +109,11 @@ const fileOps = {
|
|||||||
} else {
|
} else {
|
||||||
const errorData = await response.text();
|
const errorData = await response.text();
|
||||||
console.error('Upload error:', errorData);
|
console.error('Upload error:', errorData);
|
||||||
window.ui.showNotification('Error', `Error al subir el archivo: ${file.name}`);
|
window.ui.showNotification('Error', `Error uploading file: ${file.name}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Network error during upload:', error);
|
console.error('Network error during upload:', error);
|
||||||
window.ui.showNotification('Error', `Error de red al subir el archivo: ${file.name}`);
|
window.ui.showNotification('Error', `Network error uploading file: ${file.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -181,7 +181,7 @@ const fileOps = {
|
|||||||
console.log(`Created folder: ${folderPath} -> ${folder.id}`);
|
console.log(`Created folder: ${folderPath} -> ${folder.id}`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error creating folder ${folderPath}:`, await response.text());
|
console.error(`Error creating folder ${folderPath}:`, await response.text());
|
||||||
window.ui.showNotification('Error', `Error creando carpeta: ${folderName}`);
|
window.ui.showNotification('Error', `Error creating folder: ${folderName}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Network error creating folder ${folderPath}:`, error);
|
console.error(`Network error creating folder ${folderPath}:`, error);
|
||||||
@@ -228,7 +228,7 @@ const fileOps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Finish up
|
// Finish up
|
||||||
window.ui.showNotification('Carpeta subida', `${uploadedCount} archivos subidos correctamente`);
|
window.ui.showNotification('Folder uploaded', `${uploadedCount} files uploaded successfully`);
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 800));
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
console.log('Creating folder with name:', name);
|
console.log('Creating folder with name:', name);
|
||||||
|
|
||||||
// Enviar la solicitud real al backend para crear la carpeta
|
// Send the actual request to the backend to create the folder
|
||||||
const response = await fetch('/api/folders', {
|
const response = await fetch('/api/folders', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -268,28 +268,28 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Obtener la carpeta creada del backend
|
// Get the created folder from the backend
|
||||||
const folder = await response.json();
|
const folder = await response.json();
|
||||||
console.log('Folder created successfully:', folder);
|
console.log('Folder created successfully:', folder);
|
||||||
|
|
||||||
// Añadir la carpeta a la vista de inmediato para feedback instantáneo
|
// Add the folder to the view immediately for instant feedback
|
||||||
window.ui.addFolderToView(folder);
|
window.ui.addFolderToView(folder);
|
||||||
|
|
||||||
// Esperar para permitir que el backend guarde los cambios
|
// Wait to allow the backend to save the changes
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
// Recargar los archivos para refrescar la vista
|
// Reload files to refresh the view
|
||||||
await window.loadFiles({forceRefresh: true});
|
await window.loadFiles({forceRefresh: true});
|
||||||
|
|
||||||
window.ui.showNotification('Carpeta creada', `"${name}" creada correctamente`);
|
window.ui.showNotification('Folder created', `"${name}" created successfully`);
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.text();
|
const errorData = await response.text();
|
||||||
console.error('Create folder error:', errorData);
|
console.error('Create folder error:', errorData);
|
||||||
window.ui.showNotification('Error', 'Error al crear la carpeta');
|
window.ui.showNotification('Error', 'Error creating the folder');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating folder:', error);
|
console.error('Error creating folder:', error);
|
||||||
window.ui.showNotification('Error', 'Error al crear la carpeta');
|
window.ui.showNotification('Error', 'Error creating the folder');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -315,22 +315,22 @@ const fileOps = {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Reload files after moving
|
// Reload files after moving
|
||||||
await window.loadFiles();
|
await window.loadFiles();
|
||||||
window.ui.showNotification('Archivo movido', 'Archivo movido correctamente');
|
window.ui.showNotification('File moved', 'File moved successfully');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
let errorMessage = 'Error desconocido';
|
let errorMessage = 'Unknown error';
|
||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
errorMessage = errorData.error || 'Error desconocido';
|
errorMessage = errorData.error || 'Unknown error';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage = 'Error al procesar la respuesta del servidor';
|
errorMessage = 'Error processing server response';
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error al mover el archivo: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error moving the file: ${errorMessage}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error moving file:', error);
|
console.error('Error moving file:', error);
|
||||||
window.ui.showNotification('Error', 'Error al mover el archivo');
|
window.ui.showNotification('Error', 'Error moving the file');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -357,22 +357,22 @@ const fileOps = {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Reload files after moving
|
// Reload files after moving
|
||||||
await window.loadFiles();
|
await window.loadFiles();
|
||||||
window.ui.showNotification('Carpeta movida', 'Carpeta movida correctamente');
|
window.ui.showNotification('Folder moved', 'Folder moved successfully');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
let errorMessage = 'Error desconocido';
|
let errorMessage = 'Unknown error';
|
||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
errorMessage = errorData.error || 'Error desconocido';
|
errorMessage = errorData.error || 'Unknown error';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage = 'Error al procesar la respuesta del servidor';
|
errorMessage = 'Error processing server response';
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error al mover la carpeta: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error moving the folder: ${errorMessage}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error moving folder:', error);
|
console.error('Error moving folder:', error);
|
||||||
window.ui.showNotification('Error', 'Error al mover la carpeta');
|
window.ui.showNotification('Error', 'Error moving the folder');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -400,26 +400,26 @@ const fileOps = {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification(
|
window.ui.showNotification(
|
||||||
window.i18n ? window.i18n.t('notifications.file_renamed') : 'Archivo renombrado',
|
window.i18n ? window.i18n.t('notifications.file_renamed') : 'File renamed',
|
||||||
window.i18n ? window.i18n.t('notifications.file_renamed_to', { name: newName }) : `Archivo renombrado a "${newName}"`
|
window.i18n ? window.i18n.t('notifications.file_renamed_to', { name: newName }) : `File renamed to "${newName}"`
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
console.error('Error response:', errorText);
|
console.error('Error response:', errorText);
|
||||||
let errorMessage = 'Error desconocido';
|
let errorMessage = 'Unknown error';
|
||||||
try {
|
try {
|
||||||
const errorData = JSON.parse(errorText);
|
const errorData = JSON.parse(errorText);
|
||||||
errorMessage = errorData.error || response.statusText;
|
errorMessage = errorData.error || response.statusText;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage = errorText || response.statusText;
|
errorMessage = errorText || response.statusText;
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error al renombrar el archivo: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error renaming file:', error);
|
console.error('Error renaming file:', error);
|
||||||
window.ui.showNotification('Error', 'Error al renombrar el archivo');
|
window.ui.showNotification('Error', 'Error renaming the file');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -446,13 +446,13 @@ const fileOps = {
|
|||||||
console.log('Response status:', response.status);
|
console.log('Response status:', response.status);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification('Carpeta renombrada', `Carpeta renombrada a "${newName}"`);
|
window.ui.showNotification('Folder renamed', `Folder renamed to "${newName}"`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
console.error('Error response:', errorText);
|
console.error('Error response:', errorText);
|
||||||
|
|
||||||
let errorMessage = 'Error desconocido';
|
let errorMessage = 'Unknown error';
|
||||||
try {
|
try {
|
||||||
// Try to parse as JSON
|
// Try to parse as JSON
|
||||||
const errorData = JSON.parse(errorText);
|
const errorData = JSON.parse(errorText);
|
||||||
@@ -462,12 +462,12 @@ const fileOps = {
|
|||||||
errorMessage = errorText || response.statusText;
|
errorMessage = errorText || response.statusText;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ui.showNotification('Error', `Error al renombrar la carpeta: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error renaming the folder: ${errorMessage}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error renaming folder:', error);
|
console.error('Error renaming folder:', error);
|
||||||
window.ui.showNotification('Error', 'Error al renombrar la carpeta');
|
window.ui.showNotification('Error', 'Error renaming the folder');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -480,9 +480,9 @@ const fileOps = {
|
|||||||
*/
|
*/
|
||||||
async deleteFile(fileId, fileName) {
|
async deleteFile(fileId, fileName) {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Mover a papelera',
|
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
||||||
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `¿Estás seguro de que quieres mover a la papelera el archivo "${fileName}"?`,
|
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `Are you sure you want to move the file "${fileName}" to trash?`,
|
||||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Eliminar',
|
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
|
||||||
});
|
});
|
||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -495,7 +495,7 @@ const fileOps = {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
window.ui.showNotification('Archivo movido a papelera', `"${fileName}" movido a la papelera`);
|
window.ui.showNotification('File moved to trash', `"${fileName}" moved to trash`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// Fallback to direct deletion if trash fails
|
// Fallback to direct deletion if trash fails
|
||||||
@@ -506,16 +506,16 @@ const fileOps = {
|
|||||||
|
|
||||||
if (fallbackResponse.ok) {
|
if (fallbackResponse.ok) {
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
window.ui.showNotification('Archivo eliminado', `"${fileName}" eliminado correctamente`);
|
window.ui.showNotification('File deleted', `"${fileName}" deleted successfully`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error al eliminar el archivo');
|
window.ui.showNotification('Error', 'Error deleting the file');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting file:', error);
|
console.error('Error deleting file:', error);
|
||||||
window.ui.showNotification('Error', 'Error al eliminar el archivo');
|
window.ui.showNotification('Error', 'Error deleting the file');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -528,9 +528,9 @@ const fileOps = {
|
|||||||
*/
|
*/
|
||||||
async deleteFolder(folderId, folderName) {
|
async deleteFolder(folderId, folderName) {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Mover a papelera',
|
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
||||||
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_folder', { name: folderName }) : `¿Estás seguro de que quieres mover a la papelera la carpeta "${folderName}" y todo su contenido?`,
|
message: window.i18n ? window.i18n.t('dialogs.confirm_delete_folder', { name: folderName }) : `Are you sure you want to move the folder "${folderName}" and all its contents to trash?`,
|
||||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Eliminar',
|
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete',
|
||||||
});
|
});
|
||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -548,7 +548,7 @@ const fileOps = {
|
|||||||
window.ui.updateBreadcrumb('');
|
window.ui.updateBreadcrumb('');
|
||||||
}
|
}
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
window.ui.showNotification('Carpeta movida a papelera', `"${folderName}" movida a la papelera`);
|
window.ui.showNotification('Folder moved to trash', `"${folderName}" moved to trash`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// Fallback to direct deletion if trash fails
|
// Fallback to direct deletion if trash fails
|
||||||
@@ -564,23 +564,23 @@ const fileOps = {
|
|||||||
window.ui.updateBreadcrumb('');
|
window.ui.updateBreadcrumb('');
|
||||||
}
|
}
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
window.ui.showNotification('Carpeta eliminada', `"${folderName}" eliminada correctamente`);
|
window.ui.showNotification('Folder deleted', `"${folderName}" deleted successfully`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error al eliminar la carpeta');
|
window.ui.showNotification('Error', 'Error deleting the folder');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting folder:', error);
|
console.error('Error deleting folder:', error);
|
||||||
window.ui.showNotification('Error', 'Error al eliminar la carpeta');
|
window.ui.showNotification('Error', 'Error deleting the folder');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtener elementos de la papelera
|
* Get trash items
|
||||||
* @returns {Promise<Array>} - Lista de elementos en la papelera
|
* @returns {Promise<Array>} - List of trash items
|
||||||
*/
|
*/
|
||||||
async getTrashItems() {
|
async getTrashItems() {
|
||||||
try {
|
try {
|
||||||
@@ -601,9 +601,9 @@ const fileOps = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restaurar un elemento desde la papelera
|
* Restore an item from trash
|
||||||
* @param {string} trashId - ID del elemento en la papelera
|
* @param {string} trashId - Trash item ID
|
||||||
* @returns {Promise<boolean>} - Éxito de la operación
|
* @returns {Promise<boolean>} - Operation success
|
||||||
*/
|
*/
|
||||||
async restoreFromTrash(trashId) {
|
async restoreFromTrash(trashId) {
|
||||||
try {
|
try {
|
||||||
@@ -617,29 +617,29 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification('Elemento restaurado', 'Elemento restaurado correctamente');
|
window.ui.showNotification('Item restored', 'Item restored successfully');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error al restaurar el elemento');
|
window.ui.showNotification('Error', 'Error restoring the item');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error restoring item from trash:', error);
|
console.error('Error restoring item from trash:', error);
|
||||||
window.ui.showNotification('Error', 'Error al restaurar el elemento');
|
window.ui.showNotification('Error', 'Error restoring the item');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Eliminar permanentemente un elemento de la papelera
|
* Permanently delete a trash item
|
||||||
* @param {string} trashId - ID del elemento en la papelera
|
* @param {string} trashId - Trash item ID
|
||||||
* @returns {Promise<boolean>} - Éxito de la operación
|
* @returns {Promise<boolean>} - Operation success
|
||||||
*/
|
*/
|
||||||
async deletePermanently(trashId) {
|
async deletePermanently(trashId) {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete') : 'Eliminar permanentemente',
|
title: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete') : 'Delete permanently',
|
||||||
message: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete_msg') : '¿Estás seguro de que quieres eliminar permanentemente este elemento? Esta acción no se puede deshacer.',
|
message: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete_msg') : 'Are you sure you want to permanently delete this item? This action cannot be undone.',
|
||||||
confirmText: window.i18n ? window.i18n.t('actions.delete_permanently') : 'Eliminar permanentemente',
|
confirmText: window.i18n ? window.i18n.t('actions.delete_permanently') : 'Delete permanently',
|
||||||
});
|
});
|
||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -650,28 +650,28 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification('Elemento eliminado', 'Elemento eliminado permanentemente');
|
window.ui.showNotification('Item deleted', 'Item permanently deleted');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error al eliminar el elemento');
|
window.ui.showNotification('Error', 'Error deleting the item');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting item permanently:', error);
|
console.error('Error deleting item permanently:', error);
|
||||||
window.ui.showNotification('Error', 'Error al eliminar el elemento');
|
window.ui.showNotification('Error', 'Error deleting the item');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vaciar la papelera
|
* Empty the trash
|
||||||
* @returns {Promise<boolean>} - Éxito de la operación
|
* @returns {Promise<boolean>} - Operation success
|
||||||
*/
|
*/
|
||||||
async emptyTrash() {
|
async emptyTrash() {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: window.i18n ? window.i18n.t('dialogs.confirm_empty_trash') : 'Vaciar papelera',
|
title: window.i18n ? window.i18n.t('dialogs.confirm_empty_trash') : 'Empty trash',
|
||||||
message: 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.',
|
message: window.i18n ? window.i18n.t('trash.empty_confirm') : 'Are you sure you want to empty the trash? This action will permanently delete all items.',
|
||||||
confirmText: window.i18n ? window.i18n.t('actions.empty_trash') : 'Vaciar papelera',
|
confirmText: window.i18n ? window.i18n.t('actions.empty_trash') : 'Empty trash',
|
||||||
});
|
});
|
||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -682,23 +682,23 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification('Papelera vaciada', 'La papelera ha sido vaciada correctamente');
|
window.ui.showNotification('Trash emptied', 'The trash has been emptied successfully');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error al vaciar la papelera');
|
window.ui.showNotification('Error', 'Error emptying the trash');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error emptying trash:', error);
|
console.error('Error emptying trash:', error);
|
||||||
window.ui.showNotification('Error', 'Error al vaciar la papelera');
|
window.ui.showNotification('Error', 'Error emptying the trash');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Descargar un archivo
|
* Download a file
|
||||||
* @param {string} fileId - ID del archivo
|
* @param {string} fileId - File ID
|
||||||
* @param {string} fileName - Nombre del archivo
|
* @param {string} fileName - File name
|
||||||
*/
|
*/
|
||||||
async downloadFile(fileId, fileName) {
|
async downloadFile(fileId, fileName) {
|
||||||
try {
|
try {
|
||||||
@@ -716,23 +716,23 @@ const fileOps = {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error al descargar el archivo');
|
window.ui.showNotification('Error', 'Error downloading the file');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error downloading file:', error);
|
console.error('Error downloading file:', error);
|
||||||
window.ui.showNotification('Error', 'Error al descargar el archivo');
|
window.ui.showNotification('Error', 'Error downloading the file');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Descargar una carpeta como ZIP
|
* Download a folder as ZIP
|
||||||
* @param {string} folderId - ID de la carpeta
|
* @param {string} folderId - Folder ID
|
||||||
* @param {string} folderName - Nombre de la carpeta
|
* @param {string} folderName - Folder name
|
||||||
*/
|
*/
|
||||||
async downloadFolder(folderId, folderName) {
|
async downloadFolder(folderId, folderName) {
|
||||||
try {
|
try {
|
||||||
// Show notification to user
|
// Show notification to user
|
||||||
window.ui.showNotification('Preparando descarga', 'Preparando la carpeta para descargar...');
|
window.ui.showNotification('Preparing download', 'Preparing the folder for download...');
|
||||||
|
|
||||||
const response = await fetch(`/api/folders/${folderId}/download?format=zip`, {
|
const response = await fetch(`/api/folders/${folderId}/download?format=zip`, {
|
||||||
headers: getAuthHeaders()
|
headers: getAuthHeaders()
|
||||||
@@ -748,11 +748,11 @@ const fileOps = {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error al descargar la carpeta');
|
window.ui.showNotification('Error', 'Error downloading the folder');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error downloading folder:', error);
|
console.error('Error downloading folder:', error);
|
||||||
window.ui.showNotification('Error', 'Error al descargar la carpeta');
|
window.ui.showNotification('Error', 'Error downloading the folder');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -214,11 +214,11 @@ const fileSharing = {
|
|||||||
copyLinkToClipboard(url) {
|
copyLinkToClipboard(url) {
|
||||||
try {
|
try {
|
||||||
navigator.clipboard.writeText(url);
|
navigator.clipboard.writeText(url);
|
||||||
window.ui.showNotification('Enlace copiado', 'Enlace copiado al portapapeles');
|
window.ui.showNotification('Link copied', 'Link copied to clipboard');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error copying to clipboard:', error);
|
console.error('Error copying to clipboard:', error);
|
||||||
window.ui.showNotification('Error', 'No se pudo copiar el enlace');
|
window.ui.showNotification('Error', 'Could not copy link');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -229,7 +229,7 @@ const fileSharing = {
|
|||||||
* @returns {string} - Formatted date string
|
* @returns {string} - Formatted date string
|
||||||
*/
|
*/
|
||||||
formatExpirationDate(dateString) {
|
formatExpirationDate(dateString) {
|
||||||
if (!dateString) return 'Sin vencimiento';
|
if (!dateString) return 'No expiration';
|
||||||
|
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||||
@@ -252,11 +252,11 @@ const fileSharing = {
|
|||||||
// Simulate network delay
|
// Simulate network delay
|
||||||
//await new Promise(resolve => setTimeout(resolve, 800));
|
//await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
|
||||||
window.ui.showNotification('Notificación enviada', `Se envió notificación a ${recipientEmail}`);
|
window.ui.showNotification('Notification sent', `Notification sent to ${recipientEmail}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending share notification:', error);
|
console.error('Error sending share notification:', error);
|
||||||
window.ui.showNotification('Error', 'No se pudo enviar la notificación');
|
window.ui.showNotification('Error', 'Could not send notification');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+11
-11
@@ -110,10 +110,10 @@ const recent = {
|
|||||||
filesGrid.innerHTML = '';
|
filesGrid.innerHTML = '';
|
||||||
filesListView.innerHTML = `
|
filesListView.innerHTML = `
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div data-i18n="files.name">Nombre</div>
|
<div data-i18n="files.name">Name</div>
|
||||||
<div data-i18n="files.type">Tipo</div>
|
<div data-i18n="files.type">Type</div>
|
||||||
<div data-i18n="files.size">Tamaño</div>
|
<div data-i18n="files.size">Size</div>
|
||||||
<div data-i18n="files.last_accessed">Último acceso</div>
|
<div data-i18n="files.last_accessed">Last accessed</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -126,8 +126,8 @@ const recent = {
|
|||||||
emptyState.className = 'empty-state';
|
emptyState.className = 'empty-state';
|
||||||
emptyState.innerHTML = `
|
emptyState.innerHTML = `
|
||||||
<i class="fas fa-clock" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
<i class="fas fa-clock" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
||||||
<p>${window.i18n ? window.i18n.t('recent.empty_state') : 'No hay archivos recientes'}</p>
|
<p>${window.i18n ? window.i18n.t('recent.empty_state') : 'No recent files'}</p>
|
||||||
<p>${window.i18n ? window.i18n.t('recent.empty_hint') : 'Los archivos que abras aparecerán aquí'}</p>
|
<p>${window.i18n ? window.i18n.t('recent.empty_hint') : 'Files you open will appear here'}</p>
|
||||||
`;
|
`;
|
||||||
filesGrid.appendChild(emptyState);
|
filesGrid.appendChild(emptyState);
|
||||||
return;
|
return;
|
||||||
@@ -143,7 +143,7 @@ const recent = {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error displaying recent files:', error);
|
console.error('Error displaying recent files:', error);
|
||||||
window.ui.showNotification('Error', 'Error al cargar archivos recientes');
|
window.ui.showNotification('Error', 'Error loading recent files');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -157,17 +157,17 @@ const recent = {
|
|||||||
// Determine icon and type
|
// Determine icon and type
|
||||||
let iconClass = 'fas fa-file';
|
let iconClass = 'fas fa-file';
|
||||||
let iconSpecialClass = '';
|
let iconSpecialClass = '';
|
||||||
let typeLabel = 'Documento';
|
let typeLabel = 'Document';
|
||||||
|
|
||||||
if (file.mime_type) {
|
if (file.mime_type) {
|
||||||
if (file.mime_type.startsWith('image/')) {
|
if (file.mime_type.startsWith('image/')) {
|
||||||
iconClass = 'fas fa-file-image';
|
iconClass = 'fas fa-file-image';
|
||||||
iconSpecialClass = 'image-icon';
|
iconSpecialClass = 'image-icon';
|
||||||
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Imagen';
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Image';
|
||||||
} else if (file.mime_type.startsWith('text/')) {
|
} else if (file.mime_type.startsWith('text/')) {
|
||||||
iconClass = 'fas fa-file-alt';
|
iconClass = 'fas fa-file-alt';
|
||||||
iconSpecialClass = 'text-icon';
|
iconSpecialClass = 'text-icon';
|
||||||
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Texto';
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Text';
|
||||||
} else if (file.mime_type.startsWith('video/')) {
|
} else if (file.mime_type.startsWith('video/')) {
|
||||||
iconClass = 'fas fa-file-video';
|
iconClass = 'fas fa-file-video';
|
||||||
iconSpecialClass = 'video-icon';
|
iconSpecialClass = 'video-icon';
|
||||||
@@ -204,7 +204,7 @@ const recent = {
|
|||||||
<i class="${iconClass}"></i>
|
<i class="${iconClass}"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-name">${file.name}</div>
|
<div class="file-name">${file.name}</div>
|
||||||
<div class="file-info">Accedido ${formattedDate.split(' ')[0]}</div>
|
<div class="file-info">Accessed ${formattedDate.split(' ')[0]}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Download on click
|
// Download on click
|
||||||
|
|||||||
+12
-12
@@ -52,7 +52,7 @@ const search = {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error performing search:', error);
|
console.error('Error performing search:', error);
|
||||||
window.ui.showNotification('Error', 'Error al realizar la búsqueda');
|
window.ui.showNotification('Error', 'Error performing search');
|
||||||
return { files: [], folders: [], total_count: 0 };
|
return { files: [], folders: [], total_count: 0 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -91,7 +91,7 @@ const search = {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error performing advanced search:', error);
|
console.error('Error performing advanced search:', error);
|
||||||
window.ui.showNotification('Error', 'Error al realizar la búsqueda avanzada');
|
window.ui.showNotification('Error', 'Error performing advanced search');
|
||||||
return { files: [], folders: [], total_count: 0 };
|
return { files: [], folders: [], total_count: 0 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -109,10 +109,10 @@ const search = {
|
|||||||
filesGrid.innerHTML = '';
|
filesGrid.innerHTML = '';
|
||||||
filesListView.innerHTML = `
|
filesListView.innerHTML = `
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div data-i18n="files.name">Nombre</div>
|
<div data-i18n="files.name">Name</div>
|
||||||
<div data-i18n="files.type">Tipo</div>
|
<div data-i18n="files.type">Type</div>
|
||||||
<div data-i18n="files.size">Tamaño</div>
|
<div data-i18n="files.size">Size</div>
|
||||||
<div data-i18n="files.modified">Modificado</div>
|
<div data-i18n="files.modified">Modified</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -120,9 +120,9 @@ const search = {
|
|||||||
const searchHeader = document.createElement('div');
|
const searchHeader = document.createElement('div');
|
||||||
searchHeader.className = 'search-results-header';
|
searchHeader.className = 'search-results-header';
|
||||||
searchHeader.innerHTML = `
|
searchHeader.innerHTML = `
|
||||||
<h3>Resultados de búsqueda (${results.total_count || (results.files.length + results.folders.length)})</h3>
|
<h3>Search results (${results.total_count || (results.files.length + results.folders.length)})</h3>
|
||||||
<button class="btn btn-secondary" id="clear-search-btn">
|
<button class="btn btn-secondary" id="clear-search-btn">
|
||||||
<i class="fas fa-times"></i> Limpiar búsqueda
|
<i class="fas fa-times"></i> Clear search
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
filesGrid.appendChild(searchHeader);
|
filesGrid.appendChild(searchHeader);
|
||||||
@@ -147,7 +147,7 @@ const search = {
|
|||||||
emptyState.className = 'empty-state';
|
emptyState.className = 'empty-state';
|
||||||
emptyState.innerHTML = `
|
emptyState.innerHTML = `
|
||||||
<i class="fas fa-search" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
<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>
|
<p>No results found for this search</p>
|
||||||
`;
|
`;
|
||||||
filesGrid.appendChild(emptyState);
|
filesGrid.appendChild(emptyState);
|
||||||
return;
|
return;
|
||||||
@@ -178,15 +178,15 @@ const search = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.ui.showNotification('Caché limpiada', 'Caché de búsqueda limpiada correctamente');
|
window.ui.showNotification('Cache cleared', 'Search cache cleared successfully');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
window.ui.showNotification('Error', 'Error al limpiar la caché de búsqueda');
|
window.ui.showNotification('Error', 'Error clearing search cache');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing search cache:', error);
|
console.error('Error clearing search cache:', error);
|
||||||
window.ui.showNotification('Error', 'Error al limpiar la caché de búsqueda');
|
window.ui.showNotification('Error', 'Error clearing search cache');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-66
@@ -16,24 +16,24 @@ const ui = {
|
|||||||
folderMenu.id = 'folder-context-menu';
|
folderMenu.id = 'folder-context-menu';
|
||||||
folderMenu.innerHTML = `
|
folderMenu.innerHTML = `
|
||||||
<div class="context-menu-item" id="download-folder-option">
|
<div class="context-menu-item" id="download-folder-option">
|
||||||
<i class="fas fa-download"></i> <span data-i18n="actions.download">Descargar</span>
|
<i class="fas fa-download"></i> <span data-i18n="actions.download">Download</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-item" id="favorite-folder-option">
|
<div class="context-menu-item" id="favorite-folder-option">
|
||||||
<i class="fas fa-star"></i> <span data-i18n="actions.favorite">Añadir a favoritos</span>
|
<i class="fas fa-star"></i> <span data-i18n="actions.favorite">Add to favorites</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-item" id="share-folder-option">
|
<div class="context-menu-item" id="share-folder-option">
|
||||||
<i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Compartir</span>
|
<i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Share</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
<div class="context-menu-item" id="rename-folder-option">
|
<div class="context-menu-item" id="rename-folder-option">
|
||||||
<i class="fas fa-pen"></i> <span data-i18n="actions.rename">Renombrar</span>
|
<i class="fas fa-pen"></i> <span data-i18n="actions.rename">Rename</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-item" id="move-folder-option">
|
<div class="context-menu-item" id="move-folder-option">
|
||||||
<i class="fas fa-arrows-alt"></i> <span data-i18n="actions.move">Mover a...</span>
|
<i class="fas fa-arrows-alt"></i> <span data-i18n="actions.move">Move to...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
<div class="context-menu-item context-menu-item-danger" id="delete-folder-option">
|
<div class="context-menu-item context-menu-item-danger" id="delete-folder-option">
|
||||||
<i class="fas fa-trash-alt"></i> <span data-i18n="actions.delete">Eliminar</span>
|
<i class="fas fa-trash-alt"></i> <span data-i18n="actions.delete">Delete</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(folderMenu);
|
document.body.appendChild(folderMenu);
|
||||||
@@ -46,28 +46,28 @@ const ui = {
|
|||||||
fileMenu.id = 'file-context-menu';
|
fileMenu.id = 'file-context-menu';
|
||||||
fileMenu.innerHTML = `
|
fileMenu.innerHTML = `
|
||||||
<div class="context-menu-item" id="view-file-option">
|
<div class="context-menu-item" id="view-file-option">
|
||||||
<i class="fas fa-eye"></i> <span data-i18n="actions.view">Ver</span>
|
<i class="fas fa-eye"></i> <span data-i18n="actions.view">View</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-item" id="download-file-option">
|
<div class="context-menu-item" id="download-file-option">
|
||||||
<i class="fas fa-download"></i> <span data-i18n="actions.download">Descargar</span>
|
<i class="fas fa-download"></i> <span data-i18n="actions.download">Download</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
<div class="context-menu-item" id="favorite-file-option">
|
<div class="context-menu-item" id="favorite-file-option">
|
||||||
<i class="fas fa-star"></i> <span data-i18n="actions.favorite">Añadir a favoritos</span>
|
<i class="fas fa-star"></i> <span data-i18n="actions.favorite">Add to favorites</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-item" id="share-file-option">
|
<div class="context-menu-item" id="share-file-option">
|
||||||
<i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Compartir</span>
|
<i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Share</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
<div class="context-menu-item" id="rename-file-option">
|
<div class="context-menu-item" id="rename-file-option">
|
||||||
<i class="fas fa-pen"></i> <span data-i18n="actions.rename">Renombrar</span>
|
<i class="fas fa-pen"></i> <span data-i18n="actions.rename">Rename</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-item" id="move-file-option">
|
<div class="context-menu-item" id="move-file-option">
|
||||||
<i class="fas fa-arrows-alt"></i> <span data-i18n="actions.move">Mover a...</span>
|
<i class="fas fa-arrows-alt"></i> <span data-i18n="actions.move">Move to...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
<div class="context-menu-item context-menu-item-danger" id="delete-file-option">
|
<div class="context-menu-item context-menu-item-danger" id="delete-file-option">
|
||||||
<i class="fas fa-trash-alt"></i> <span data-i18n="actions.delete">Eliminar</span>
|
<i class="fas fa-trash-alt"></i> <span data-i18n="actions.delete">Delete</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(fileMenu);
|
document.body.appendChild(fileMenu);
|
||||||
@@ -82,14 +82,14 @@ const ui = {
|
|||||||
<div class="rename-dialog-content">
|
<div class="rename-dialog-content">
|
||||||
<div class="rename-dialog-header">
|
<div class="rename-dialog-header">
|
||||||
<i class="fas fa-pen" style="color:#ff5e3a"></i>
|
<i class="fas fa-pen" style="color:#ff5e3a"></i>
|
||||||
<span data-i18n="dialogs.rename_folder">Renombrar</span>
|
<span data-i18n="dialogs.rename_folder">Rename</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="rename-dialog-body">
|
<div class="rename-dialog-body">
|
||||||
<input type="text" id="rename-input" data-i18n-placeholder="dialogs.new_name" placeholder="Nuevo nombre">
|
<input type="text" id="rename-input" data-i18n-placeholder="dialogs.new_name" placeholder="New name">
|
||||||
</div>
|
</div>
|
||||||
<div class="rename-dialog-buttons">
|
<div class="rename-dialog-buttons">
|
||||||
<button class="btn btn-secondary" id="rename-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
|
<button class="btn btn-secondary" id="rename-cancel-btn" data-i18n="actions.cancel">Cancel</button>
|
||||||
<button class="btn btn-primary" id="rename-confirm-btn" data-i18n="actions.rename">Renombrar</button>
|
<button class="btn btn-primary" id="rename-confirm-btn" data-i18n="actions.rename">Rename</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -105,19 +105,19 @@ const ui = {
|
|||||||
<div class="rename-dialog-content">
|
<div class="rename-dialog-content">
|
||||||
<div class="rename-dialog-header">
|
<div class="rename-dialog-header">
|
||||||
<i class="fas fa-arrows-alt" style="color:#ff5e3a"></i>
|
<i class="fas fa-arrows-alt" style="color:#ff5e3a"></i>
|
||||||
<span data-i18n="dialogs.move_file">Mover</span>
|
<span data-i18n="dialogs.move_file">Move</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="rename-dialog-body">
|
<div class="rename-dialog-body">
|
||||||
<p style="margin:0 0 12px;color:#718096;font-size:14px" data-i18n="dialogs.select_destination">Selecciona la carpeta destino:</p>
|
<p style="margin:0 0 12px;color:#718096;font-size:14px" data-i18n="dialogs.select_destination">Select destination folder:</p>
|
||||||
<div id="folder-select-container" style="max-height:220px;overflow-y:auto;">
|
<div id="folder-select-container" style="max-height:220px;overflow-y:auto;">
|
||||||
<div class="folder-select-item selected" data-folder-id="">
|
<div class="folder-select-item selected" data-folder-id="">
|
||||||
<i class="fas fa-folder"></i> <span data-i18n="dialogs.root">Raíz</span>
|
<i class="fas fa-folder"></i> <span data-i18n="dialogs.root">Root</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rename-dialog-buttons">
|
<div class="rename-dialog-buttons">
|
||||||
<button class="btn btn-secondary" id="move-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
|
<button class="btn btn-secondary" id="move-cancel-btn" data-i18n="actions.cancel">Cancel</button>
|
||||||
<button class="btn btn-primary" id="move-confirm-btn" data-i18n="actions.move_to">Mover</button>
|
<button class="btn btn-primary" id="move-confirm-btn" data-i18n="actions.move_to">Move</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -133,67 +133,67 @@ const ui = {
|
|||||||
<div class="share-dialog-content">
|
<div class="share-dialog-content">
|
||||||
<div class="share-dialog-header">
|
<div class="share-dialog-header">
|
||||||
<i class="fas fa-share-alt" style="color:#ff5e3a"></i>
|
<i class="fas fa-share-alt" style="color:#ff5e3a"></i>
|
||||||
<span data-i18n="dialogs.share_file">Compartir archivo</span>
|
<span data-i18n="dialogs.share_file">Share file</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="shared-item-info">
|
<div class="shared-item-info">
|
||||||
<strong>Elemento:</strong> <span id="shared-item-name"></span>
|
<strong>Item:</strong> <span id="shared-item-name"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="existing-shares-section" style="display:none; margin: 15px 0;">
|
<div id="existing-shares-section" style="display:none; margin: 15px 0;">
|
||||||
<h3 data-i18n="dialogs.existing_shares">Enlaces compartidos existentes</h3>
|
<h3 data-i18n="dialogs.existing_shares">Existing shared links</h3>
|
||||||
<div id="existing-shares-container"></div>
|
<div id="existing-shares-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="share-options">
|
<div class="share-options">
|
||||||
<h3 data-i18n="dialogs.share_options">Opciones de compartición</h3>
|
<h3 data-i18n="dialogs.share_options">Share options</h3>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="share-password" data-i18n="dialogs.password">Contraseña (opcional):</label>
|
<label for="share-password" data-i18n="dialogs.password">Password (optional):</label>
|
||||||
<input type="password" id="share-password" placeholder="Proteger con contraseña">
|
<input type="password" id="share-password" placeholder="Protect with password">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="share-expiration" data-i18n="dialogs.expiration">Fecha de vencimiento (opcional):</label>
|
<label for="share-expiration" data-i18n="dialogs.expiration">Expiration date (optional):</label>
|
||||||
<input type="date" id="share-expiration">
|
<input type="date" id="share-expiration">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label data-i18n="dialogs.permissions">Permisos:</label>
|
<label data-i18n="dialogs.permissions">Permissions:</label>
|
||||||
<div class="permission-options">
|
<div class="permission-options">
|
||||||
<div class="permission-option">
|
<div class="permission-option">
|
||||||
<input type="checkbox" id="share-permission-read" checked>
|
<input type="checkbox" id="share-permission-read" checked>
|
||||||
<label for="share-permission-read" data-i18n="permissions.read">Lectura</label>
|
<label for="share-permission-read" data-i18n="permissions.read">Read</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="permission-option">
|
<div class="permission-option">
|
||||||
<input type="checkbox" id="share-permission-write">
|
<input type="checkbox" id="share-permission-write">
|
||||||
<label for="share-permission-write" data-i18n="permissions.write">Escritura</label>
|
<label for="share-permission-write" data-i18n="permissions.write">Write</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="permission-option">
|
<div class="permission-option">
|
||||||
<input type="checkbox" id="share-permission-reshare">
|
<input type="checkbox" id="share-permission-reshare">
|
||||||
<label for="share-permission-reshare" data-i18n="permissions.reshare">Permitir compartir</label>
|
<label for="share-permission-reshare" data-i18n="permissions.reshare">Allow sharing</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="new-share-section" style="display:none; margin: 15px 0;">
|
<div id="new-share-section" style="display:none; margin: 15px 0;">
|
||||||
<h3 data-i18n="dialogs.generated_link">Enlace generado</h3>
|
<h3 data-i18n="dialogs.generated_link">Generated link</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" id="generated-share-url" readonly>
|
<input type="text" id="generated-share-url" readonly>
|
||||||
<div class="share-link-actions">
|
<div class="share-link-actions">
|
||||||
<button class="btn btn-small" id="copy-share-btn">
|
<button class="btn btn-small" id="copy-share-btn">
|
||||||
<i class="fas fa-copy"></i> <span data-i18n="actions.copy">Copiar</span>
|
<i class="fas fa-copy"></i> <span data-i18n="actions.copy">Copy</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-small" id="notify-share-btn">
|
<button class="btn btn-small" id="notify-share-btn">
|
||||||
<i class="fas fa-envelope"></i> <span data-i18n="actions.notify">Notificar</span>
|
<i class="fas fa-envelope"></i> <span data-i18n="actions.notify">Notify</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="share-dialog-buttons">
|
<div class="share-dialog-buttons">
|
||||||
<button class="btn btn-secondary" id="share-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
|
<button class="btn btn-secondary" id="share-cancel-btn" data-i18n="actions.cancel">Cancel</button>
|
||||||
<button class="btn btn-primary" id="share-confirm-btn" data-i18n="actions.share">Compartir</button>
|
<button class="btn btn-primary" id="share-confirm-btn" data-i18n="actions.share">Share</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -228,24 +228,24 @@ const ui = {
|
|||||||
<div class="share-dialog-content">
|
<div class="share-dialog-content">
|
||||||
<div class="share-dialog-header">
|
<div class="share-dialog-header">
|
||||||
<i class="fas fa-envelope" style="color:#ff5e3a"></i>
|
<i class="fas fa-envelope" style="color:#ff5e3a"></i>
|
||||||
<span data-i18n="dialogs.notify">Notificar enlace compartido</span>
|
<span data-i18n="dialogs.notify">Notify shared link</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p><strong>URL:</strong> <span id="notification-share-url"></span></p>
|
<p><strong>URL:</strong> <span id="notification-share-url"></span></p>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="notification-email" data-i18n="dialogs.recipient">Destinatario:</label>
|
<label for="notification-email" data-i18n="dialogs.recipient">Recipient:</label>
|
||||||
<input type="email" id="notification-email" placeholder="Correo electrónico">
|
<input type="email" id="notification-email" placeholder="Email address">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="notification-message" data-i18n="dialogs.message">Mensaje (opcional):</label>
|
<label for="notification-message" data-i18n="dialogs.message">Message (optional):</label>
|
||||||
<textarea id="notification-message" rows="3"></textarea>
|
<textarea id="notification-message" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="share-dialog-buttons">
|
<div class="share-dialog-buttons">
|
||||||
<button class="btn btn-secondary" id="notification-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
|
<button class="btn btn-secondary" id="notification-cancel-btn" data-i18n="actions.cancel">Cancel</button>
|
||||||
<button class="btn btn-primary" id="notification-send-btn" data-i18n="actions.send">Enviar</button>
|
<button class="btn btn-primary" id="notification-send-btn" data-i18n="actions.send">Send</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -402,7 +402,7 @@ const ui = {
|
|||||||
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
|
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
|
||||||
|
|
||||||
// For searching, we might have a custom breadcrumb text
|
// For searching, we might have a custom breadcrumb text
|
||||||
if (folderName && folderName.startsWith('Búsqueda:')) {
|
if (folderName && folderName.startsWith('Search:')) {
|
||||||
// We're in search mode - don't add click handler
|
// We're in search mode - don't add click handler
|
||||||
breadcrumb.appendChild(homeItem);
|
breadcrumb.appendChild(homeItem);
|
||||||
return;
|
return;
|
||||||
@@ -421,7 +421,7 @@ const ui = {
|
|||||||
breadcrumb.appendChild(homeItem);
|
breadcrumb.appendChild(homeItem);
|
||||||
|
|
||||||
// If we have a subfolder, add it to the breadcrumb
|
// If we have a subfolder, add it to the breadcrumb
|
||||||
if (folderName && !folderName.startsWith('Mi Carpeta') && !folderName.startsWith('Búsqueda:')) {
|
if (folderName && !folderName.startsWith('Mi Carpeta') && !folderName.startsWith('Search:')) {
|
||||||
const separator = document.createElement('span');
|
const separator = document.createElement('span');
|
||||||
separator.className = 'breadcrumb-separator';
|
separator.className = 'breadcrumb-separator';
|
||||||
separator.textContent = '>';
|
separator.textContent = '>';
|
||||||
@@ -633,14 +633,14 @@ const ui = {
|
|||||||
* @param {Object} folder - Folder object
|
* @param {Object} folder - Folder object
|
||||||
*/
|
*/
|
||||||
addFolderToView(folder) {
|
addFolderToView(folder) {
|
||||||
// Verificar si la carpeta ya existe en la vista para evitar duplicados
|
// Check if the folder already exists in the view to avoid duplicates
|
||||||
if (document.querySelector(`.file-card[data-folder-id="${folder.id}"]`) ||
|
if (document.querySelector(`.file-card[data-folder-id="${folder.id}"]`) ||
|
||||||
document.querySelector(`.file-item[data-folder-id="${folder.id}"]`)) {
|
document.querySelector(`.file-item[data-folder-id="${folder.id}"]`)) {
|
||||||
console.log(`Carpeta ${folder.name} (${folder.id}) ya existe en la vista, no duplicando`);
|
console.log(`Folder ${folder.name} (${folder.id}) already exists in the view, not duplicating`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Añadiendo carpeta a la vista: ${folder.name} (${folder.id})`);
|
console.log(`Adding folder to the view: ${folder.name} (${folder.id})`);
|
||||||
|
|
||||||
// Grid view element
|
// Grid view element
|
||||||
const folderGridElement = document.createElement('div');
|
const folderGridElement = document.createElement('div');
|
||||||
@@ -655,7 +655,7 @@ const ui = {
|
|||||||
<i class="fas fa-folder"></i>
|
<i class="fas fa-folder"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-name">${folder.name}</div>
|
<div class="file-name">${folder.name}</div>
|
||||||
<div class="file-info">Carpeta</div>
|
<div class="file-info">Folder</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Drag and drop setup for folders
|
// Drag and drop setup for folders
|
||||||
@@ -746,7 +746,7 @@ const ui = {
|
|||||||
if (id) {
|
if (id) {
|
||||||
if (isFolder) {
|
if (isFolder) {
|
||||||
if (id === folder.id) {
|
if (id === folder.id) {
|
||||||
alert("No puedes mover una carpeta a sí misma");
|
alert("You cannot move a folder to itself");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await fileOps.moveFolder(id, folder.id);
|
await fileOps.moveFolder(id, folder.id);
|
||||||
@@ -758,7 +758,7 @@ const ui = {
|
|||||||
|
|
||||||
document.getElementById('files-grid').appendChild(folderGridElement);
|
document.getElementById('files-grid').appendChild(folderGridElement);
|
||||||
|
|
||||||
// List view element - Mejorado
|
// List view element - Improved
|
||||||
const folderListElement = document.createElement('div');
|
const folderListElement = document.createElement('div');
|
||||||
folderListElement.className = 'file-item';
|
folderListElement.className = 'file-item';
|
||||||
folderListElement.dataset.folderId = folder.id;
|
folderListElement.dataset.folderId = folder.id;
|
||||||
@@ -788,7 +788,7 @@ const ui = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mejorado: Estructura y clases para la vista de lista
|
// Improved: Structure and classes for list view
|
||||||
folderListElement.innerHTML = `
|
folderListElement.innerHTML = `
|
||||||
<div class="name-cell">
|
<div class="name-cell">
|
||||||
<div class="file-icon folder-icon">
|
<div class="file-icon folder-icon">
|
||||||
@@ -796,7 +796,7 @@ const ui = {
|
|||||||
</div>
|
</div>
|
||||||
<span>${folder.name}</span>
|
<span>${folder.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Carpeta'}</div>
|
<div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}</div>
|
||||||
<div class="size-cell">--</div>
|
<div class="size-cell">--</div>
|
||||||
<div class="date-cell">${formattedDate}</div>
|
<div class="date-cell">${formattedDate}</div>
|
||||||
`;
|
`;
|
||||||
@@ -844,7 +844,7 @@ const ui = {
|
|||||||
if (id) {
|
if (id) {
|
||||||
if (isFolder) {
|
if (isFolder) {
|
||||||
if (id === folder.id) {
|
if (id === folder.id) {
|
||||||
alert("No puedes mover una carpeta a sí misma");
|
alert("You cannot move a folder to itself");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await fileOps.moveFolder(id, folder.id);
|
await fileOps.moveFolder(id, folder.id);
|
||||||
@@ -862,29 +862,29 @@ const ui = {
|
|||||||
* @param {Object} file - File object
|
* @param {Object} file - File object
|
||||||
*/
|
*/
|
||||||
addFileToView(file) {
|
addFileToView(file) {
|
||||||
// Verificar si el archivo ya existe en la vista para evitar duplicados
|
// Check if the file already exists in the view to avoid duplicates
|
||||||
if (document.querySelector(`.file-card[data-file-id="${file.id}"]`) ||
|
if (document.querySelector(`.file-card[data-file-id="${file.id}"]`) ||
|
||||||
document.querySelector(`.file-item[data-file-id="${file.id}"]`)) {
|
document.querySelector(`.file-item[data-file-id="${file.id}"]`)) {
|
||||||
console.log(`Archivo ${file.name} (${file.id}) ya existe en la vista, no duplicando`);
|
console.log(`File ${file.name} (${file.id}) already exists in the view, not duplicating`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Añadiendo archivo a la vista: ${file.name} (${file.id})`);
|
console.log(`Adding file to the view: ${file.name} (${file.id})`);
|
||||||
|
|
||||||
// Determine icon and type
|
// Determine icon and type
|
||||||
let iconClass = 'fas fa-file';
|
let iconClass = 'fas fa-file';
|
||||||
let iconSpecialClass = '';
|
let iconSpecialClass = '';
|
||||||
let typeLabel = 'Documento';
|
let typeLabel = 'Document';
|
||||||
|
|
||||||
if (file.mime_type) {
|
if (file.mime_type) {
|
||||||
if (file.mime_type.startsWith('image/')) {
|
if (file.mime_type.startsWith('image/')) {
|
||||||
iconClass = 'fas fa-file-image';
|
iconClass = 'fas fa-file-image';
|
||||||
iconSpecialClass = 'image-icon';
|
iconSpecialClass = 'image-icon';
|
||||||
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Imagen';
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Image';
|
||||||
} else if (file.mime_type.startsWith('text/')) {
|
} else if (file.mime_type.startsWith('text/')) {
|
||||||
iconClass = 'fas fa-file-alt';
|
iconClass = 'fas fa-file-alt';
|
||||||
iconSpecialClass = 'text-icon';
|
iconSpecialClass = 'text-icon';
|
||||||
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Texto';
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Text';
|
||||||
} else if (file.mime_type.startsWith('video/')) {
|
} else if (file.mime_type.startsWith('video/')) {
|
||||||
iconClass = 'fas fa-file-video';
|
iconClass = 'fas fa-file-video';
|
||||||
iconSpecialClass = 'video-icon';
|
iconSpecialClass = 'video-icon';
|
||||||
@@ -916,7 +916,7 @@ const ui = {
|
|||||||
<i class="${iconClass}"></i>
|
<i class="${iconClass}"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-name">${file.name}</div>
|
<div class="file-name">${file.name}</div>
|
||||||
<div class="file-info">Modificado ${formattedDate.split(' ')[0]}</div>
|
<div class="file-info">Modified ${formattedDate.split(' ')[0]}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
fileGridElement.dataset.fileId = file.id;
|
fileGridElement.dataset.fileId = file.id;
|
||||||
@@ -1007,7 +1007,7 @@ const ui = {
|
|||||||
|
|
||||||
document.getElementById('files-grid').appendChild(fileGridElement);
|
document.getElementById('files-grid').appendChild(fileGridElement);
|
||||||
|
|
||||||
// List view element - Mejorado con clases específicas y diseño mejorado
|
// List view element - Improved with specific classes and enhanced layout
|
||||||
const fileListElement = document.createElement('div');
|
const fileListElement = document.createElement('div');
|
||||||
fileListElement.className = 'file-item';
|
fileListElement.className = 'file-item';
|
||||||
fileListElement.dataset.fileId = file.id;
|
fileListElement.dataset.fileId = file.id;
|
||||||
@@ -1238,9 +1238,9 @@ window.initRubberBandSelection = initRubberBandSelection;
|
|||||||
* @returns {Promise<boolean>} true if confirmed, false if cancelled
|
* @returns {Promise<boolean>} true if confirmed, false if cancelled
|
||||||
*/
|
*/
|
||||||
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) {
|
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) {
|
||||||
const ct = confirmText || (window.i18n ? window.i18n.t('actions.delete') : 'Eliminar');
|
const ct = confirmText || (window.i18n ? window.i18n.t('actions.delete') : 'Delete');
|
||||||
const cc = cancelText || (window.i18n ? window.i18n.t('actions.cancel') : 'Cancelar');
|
const cc = cancelText || (window.i18n ? window.i18n.t('actions.cancel') : 'Cancel');
|
||||||
const t = title || (window.i18n ? window.i18n.t('dialogs.confirm_title') : 'Confirmar acción');
|
const t = title || (window.i18n ? window.i18n.t('dialogs.confirm_title') : 'Confirm action');
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// Remove any previous confirm dialog
|
// Remove any previous confirm dialog
|
||||||
|
|||||||
+34
-34
@@ -62,46 +62,46 @@
|
|||||||
<div class="auth-logo-text">OxiCloud</div>
|
<div class="auth-logo-text">OxiCloud</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="auth-title" data-i18n="auth.login_title">Iniciar sesión</h2>
|
<h2 class="auth-title" data-i18n="auth.login_title">Log in</h2>
|
||||||
|
|
||||||
<div class="auth-error" id="login-error"></div>
|
<div class="auth-error" id="login-error"></div>
|
||||||
|
|
||||||
<form class="auth-form" id="login-form">
|
<form class="auth-form" id="login-form">
|
||||||
<div class="auth-input-group">
|
<div class="auth-input-group">
|
||||||
<label class="auth-label" for="login-username" data-i18n="auth.username">Usuario</label>
|
<label class="auth-label" for="login-username" data-i18n="auth.username">Username</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="login-username"
|
id="login-username"
|
||||||
class="auth-input"
|
class="auth-input"
|
||||||
data-i18n-placeholder="auth.username_placeholder"
|
data-i18n-placeholder="auth.username_placeholder"
|
||||||
placeholder="Ingresa tu nombre de usuario"
|
placeholder="Enter your username"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-input-group">
|
<div class="auth-input-group">
|
||||||
<label class="auth-label" for="login-password" data-i18n="auth.password">Contraseña</label>
|
<label class="auth-label" for="login-password" data-i18n="auth.password">Password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="login-password"
|
id="login-password"
|
||||||
class="auth-input"
|
class="auth-input"
|
||||||
data-i18n-placeholder="auth.password_placeholder"
|
data-i18n-placeholder="auth.password_placeholder"
|
||||||
placeholder="Ingresa tu contraseña"
|
placeholder="Enter your password"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="auth-button" data-i18n="auth.login_button">Iniciar sesión</button>
|
<button type="submit" class="auth-button" data-i18n="auth.login_button">Log in</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="auth-toggle">
|
<div class="auth-toggle">
|
||||||
<span data-i18n="auth.no_account">¿No tienes cuenta?</span>
|
<span data-i18n="auth.no_account">Don't have an account?</span>
|
||||||
<span class="auth-toggle-link" id="show-register" data-i18n="auth.register">Regístrate</span>
|
<span class="auth-toggle-link" id="show-register" data-i18n="auth.register">Sign up</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-toggle">
|
<div class="auth-toggle">
|
||||||
<span data-i18n="auth.admin_setup">¿Primera vez?</span>
|
<span data-i18n="auth.admin_setup">First time?</span>
|
||||||
<span class="auth-toggle-link" id="show-admin-setup" data-i18n="auth.setup">Configurar administrador</span>
|
<span class="auth-toggle-link" id="show-admin-setup" data-i18n="auth.setup">Set up administrator</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -115,20 +115,20 @@
|
|||||||
<div class="auth-logo-text">OxiCloud</div>
|
<div class="auth-logo-text">OxiCloud</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="auth-title" data-i18n="auth.register_title">Crear cuenta</h2>
|
<h2 class="auth-title" data-i18n="auth.register_title">Create account</h2>
|
||||||
|
|
||||||
<div class="auth-error" id="register-error"></div>
|
<div class="auth-error" id="register-error"></div>
|
||||||
<div class="auth-success" id="register-success"></div>
|
<div class="auth-success" id="register-success"></div>
|
||||||
|
|
||||||
<form class="auth-form" id="register-form">
|
<form class="auth-form" id="register-form">
|
||||||
<div class="auth-input-group">
|
<div class="auth-input-group">
|
||||||
<label class="auth-label" for="register-username" data-i18n="auth.username">Usuario</label>
|
<label class="auth-label" for="register-username" data-i18n="auth.username">Username</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="register-username"
|
id="register-username"
|
||||||
class="auth-input"
|
class="auth-input"
|
||||||
data-i18n-placeholder="auth.username_placeholder"
|
data-i18n-placeholder="auth.username_placeholder"
|
||||||
placeholder="Ingresa un nombre de usuario"
|
placeholder="Enter a username"
|
||||||
required
|
required
|
||||||
minlength="3"
|
minlength="3"
|
||||||
maxlength="32"
|
maxlength="32"
|
||||||
@@ -142,42 +142,42 @@
|
|||||||
id="register-email"
|
id="register-email"
|
||||||
class="auth-input"
|
class="auth-input"
|
||||||
data-i18n-placeholder="auth.email_placeholder"
|
data-i18n-placeholder="auth.email_placeholder"
|
||||||
placeholder="Ingresa tu email"
|
placeholder="Enter your email"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-input-group">
|
<div class="auth-input-group">
|
||||||
<label class="auth-label" for="register-password" data-i18n="auth.password">Contraseña</label>
|
<label class="auth-label" for="register-password" data-i18n="auth.password">Password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="register-password"
|
id="register-password"
|
||||||
class="auth-input"
|
class="auth-input"
|
||||||
data-i18n-placeholder="auth.password_placeholder"
|
data-i18n-placeholder="auth.password_placeholder"
|
||||||
placeholder="Ingresa una contraseña segura"
|
placeholder="Enter a secure password"
|
||||||
required
|
required
|
||||||
minlength="8"
|
minlength="8"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-input-group">
|
<div class="auth-input-group">
|
||||||
<label class="auth-label" for="register-password-confirm" data-i18n="auth.confirm_password">Confirmar contraseña</label>
|
<label class="auth-label" for="register-password-confirm" data-i18n="auth.confirm_password">Confirm password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="register-password-confirm"
|
id="register-password-confirm"
|
||||||
class="auth-input"
|
class="auth-input"
|
||||||
data-i18n-placeholder="auth.confirm_password_placeholder"
|
data-i18n-placeholder="auth.confirm_password_placeholder"
|
||||||
placeholder="Confirma tu contraseña"
|
placeholder="Confirm your password"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="auth-button" data-i18n="auth.register_button">Crear cuenta</button>
|
<button type="submit" class="auth-button" data-i18n="auth.register_button">Create account</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="auth-toggle">
|
<div class="auth-toggle">
|
||||||
<span data-i18n="auth.have_account">¿Ya tienes cuenta?</span>
|
<span data-i18n="auth.have_account">Already have an account?</span>
|
||||||
<span class="auth-toggle-link" id="show-login" data-i18n="auth.login">Iniciar sesión</span>
|
<span class="auth-toggle-link" id="show-login" data-i18n="auth.login">Log in</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
<div class="auth-logo-text">OxiCloud</div>
|
<div class="auth-logo-text">OxiCloud</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="auth-title" data-i18n="auth.setup_title">Configuración inicial</h2>
|
<h2 class="auth-title" data-i18n="auth.setup_title">Initial setup</h2>
|
||||||
|
|
||||||
<div class="setup-steps">
|
<div class="setup-steps">
|
||||||
<div class="setup-step">
|
<div class="setup-step">
|
||||||
@@ -200,11 +200,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="setup-step">
|
<div class="setup-step">
|
||||||
<div class="step-number">2</div>
|
<div class="step-number">2</div>
|
||||||
<div class="step-title" data-i18n="auth.setup_step2">Sistema</div>
|
<div class="step-title" data-i18n="auth.setup_step2">System</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setup-step">
|
<div class="setup-step">
|
||||||
<div class="step-number">3</div>
|
<div class="step-number">3</div>
|
||||||
<div class="step-title" data-i18n="auth.setup_step3">Completado</div>
|
<div class="step-title" data-i18n="auth.setup_step3">Completed</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
|
|
||||||
<form class="auth-form" id="admin-setup-form">
|
<form class="auth-form" id="admin-setup-form">
|
||||||
<div class="auth-input-group">
|
<div class="auth-input-group">
|
||||||
<label class="auth-label" for="admin-username" data-i18n="auth.admin_username">Usuario administrador</label>
|
<label class="auth-label" for="admin-username" data-i18n="auth.admin_username">Administrator username</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="admin-username"
|
id="admin-username"
|
||||||
@@ -224,48 +224,48 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-input-group">
|
<div class="auth-input-group">
|
||||||
<label class="auth-label" for="admin-email" data-i18n="auth.admin_email">Email administrador</label>
|
<label class="auth-label" for="admin-email" data-i18n="auth.admin_email">Administrator email</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="admin-email"
|
id="admin-email"
|
||||||
class="auth-input"
|
class="auth-input"
|
||||||
data-i18n-placeholder="auth.email_placeholder"
|
data-i18n-placeholder="auth.email_placeholder"
|
||||||
placeholder="Ingresa el email del administrador"
|
placeholder="Enter the administrator email"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-input-group">
|
<div class="auth-input-group">
|
||||||
<label class="auth-label" for="admin-password" data-i18n="auth.admin_password">Contraseña administrador</label>
|
<label class="auth-label" for="admin-password" data-i18n="auth.admin_password">Administrator password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="admin-password"
|
id="admin-password"
|
||||||
class="auth-input"
|
class="auth-input"
|
||||||
data-i18n-placeholder="auth.password_placeholder"
|
data-i18n-placeholder="auth.password_placeholder"
|
||||||
placeholder="Ingresa una contraseña segura"
|
placeholder="Enter a secure password"
|
||||||
required
|
required
|
||||||
minlength="8"
|
minlength="8"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-input-group">
|
<div class="auth-input-group">
|
||||||
<label class="auth-label" for="admin-password-confirm" data-i18n="auth.confirm_password">Confirmar contraseña</label>
|
<label class="auth-label" for="admin-password-confirm" data-i18n="auth.confirm_password">Confirm password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="admin-password-confirm"
|
id="admin-password-confirm"
|
||||||
class="auth-input"
|
class="auth-input"
|
||||||
data-i18n-placeholder="auth.confirm_password_placeholder"
|
data-i18n-placeholder="auth.confirm_password_placeholder"
|
||||||
placeholder="Confirma la contraseña"
|
placeholder="Confirm the password"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="auth-button" data-i18n="auth.create_admin">Crear administrador</button>
|
<button type="submit" class="auth-button" data-i18n="auth.create_admin">Create administrator</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="auth-toggle">
|
<div class="auth-toggle">
|
||||||
<span data-i18n="auth.back_to_login">¿Ya está configurado?</span>
|
<span data-i18n="auth.back_to_login">Already configured?</span>
|
||||||
<span class="auth-toggle-link" id="back-to-login" data-i18n="auth.login">Iniciar sesión</span>
|
<span class="auth-toggle-link" id="back-to-login" data-i18n="auth.login">Log in</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+10
-10
@@ -8,17 +8,17 @@
|
|||||||
<body>
|
<body>
|
||||||
<h1>OxiCloud Test Page</h1>
|
<h1>OxiCloud Test Page</h1>
|
||||||
<div id="result"></div>
|
<div id="result"></div>
|
||||||
<button id="createBtn">Crear carpeta nueva</button>
|
<button id="createBtn">Create new folder</button>
|
||||||
<button id="listBtn">Listar carpetas</button>
|
<button id="listBtn">List folders</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Función para mostrar resultados
|
// Function to show results
|
||||||
function showResult(data) {
|
function showResult(data) {
|
||||||
document.getElementById('result').innerHTML =
|
document.getElementById('result').innerHTML =
|
||||||
'<pre>' + JSON.stringify(data, null, 2) + '</pre>';
|
'<pre>' + JSON.stringify(data, null, 2) + '</pre>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Función para crear carpeta
|
// Function to create folder
|
||||||
async function createFolder() {
|
async function createFolder() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/folders', {
|
const response = await fetch('/api/folders', {
|
||||||
@@ -33,34 +33,34 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Respuesta de creación de carpeta:', data);
|
console.log('Folder creation response:', data);
|
||||||
showResult(data);
|
showResult(data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al crear carpeta:', error);
|
console.error('Error creating folder:', error);
|
||||||
showResult({error: error.message});
|
showResult({error: error.message});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Función para listar carpetas
|
// Function to list folders
|
||||||
async function listFolders() {
|
async function listFolders() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/folders');
|
const response = await fetch('/api/folders');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Listado de carpetas:', data);
|
console.log('Folder listing:', data);
|
||||||
showResult(data);
|
showResult(data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al listar carpetas:', error);
|
console.error('Error listing folders:', error);
|
||||||
showResult({error: error.message});
|
showResult({error: error.message});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asignar eventos a botones
|
// Assign events to buttons
|
||||||
document.getElementById('createBtn').addEventListener('click', createFolder);
|
document.getElementById('createBtn').addEventListener('click', createFolder);
|
||||||
document.getElementById('listBtn').addEventListener('click', listFolders);
|
document.getElementById('listBtn').addEventListener('click', listFolders);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+10
-2
@@ -1,11 +1,19 @@
|
|||||||
{
|
{
|
||||||
"path_to_id": {
|
"path_to_id": {
|
||||||
|
"/Mi Carpeta - testuser999": "7c1afffd-867f-4649-a0f6-4cc8a9979af4",
|
||||||
"/Mi Carpeta - test": "edfc23e0-d9f9-4cc7-b1dc-6369568b19e6",
|
"/Mi Carpeta - test": "edfc23e0-d9f9-4cc7-b1dc-6369568b19e6",
|
||||||
"/Mi Carpeta - testuser": "304384d6-9a7e-4b22-a659-287957e8b667"
|
"/Mi Carpeta - user1": "b775c641-a698-458b-a561-42ae93073c74",
|
||||||
|
"/Mi Carpeta - testadmin": "02aed53b-f194-4e28-8680-2cc81a3584b3",
|
||||||
|
"/Mi Carpeta - testuser": "304384d6-9a7e-4b22-a659-287957e8b667",
|
||||||
|
"/Mi Carpeta - admin": "562e90aa-a3fc-4672-a0fd-9187fef5d4f1"
|
||||||
},
|
},
|
||||||
"id_to_path": {
|
"id_to_path": {
|
||||||
|
"562e90aa-a3fc-4672-a0fd-9187fef5d4f1": "/Mi Carpeta - admin",
|
||||||
|
"7c1afffd-867f-4649-a0f6-4cc8a9979af4": "/Mi Carpeta - testuser999",
|
||||||
|
"02aed53b-f194-4e28-8680-2cc81a3584b3": "/Mi Carpeta - testadmin",
|
||||||
"304384d6-9a7e-4b22-a659-287957e8b667": "/Mi Carpeta - testuser",
|
"304384d6-9a7e-4b22-a659-287957e8b667": "/Mi Carpeta - testuser",
|
||||||
|
"b775c641-a698-458b-a561-42ae93073c74": "/Mi Carpeta - user1",
|
||||||
"edfc23e0-d9f9-4cc7-b1dc-6369568b19e6": "/Mi Carpeta - test"
|
"edfc23e0-d9f9-4cc7-b1dc-6369568b19e6": "/Mi Carpeta - test"
|
||||||
},
|
},
|
||||||
"version": 2
|
"version": 6
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user