configuring backend topology
This commit is contained in:
@@ -5,4 +5,5 @@ pub mod pagination;
|
||||
pub mod user_dto;
|
||||
pub mod trash_dto;
|
||||
pub mod search_dto;
|
||||
pub mod share_dto;
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::domain::entities::share::{Share, ShareItemType, SharePermissions};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ShareDto {
|
||||
pub id: String,
|
||||
pub item_id: String,
|
||||
pub item_type: String,
|
||||
pub token: String,
|
||||
pub url: String,
|
||||
pub has_password: bool,
|
||||
pub expires_at: Option<u64>,
|
||||
pub permissions: SharePermissionsDto,
|
||||
pub created_at: u64,
|
||||
pub created_by: String,
|
||||
pub access_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SharePermissionsDto {
|
||||
pub read: bool,
|
||||
pub write: bool,
|
||||
pub reshare: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateShareDto {
|
||||
pub item_id: String,
|
||||
pub item_type: String,
|
||||
pub password: Option<String>,
|
||||
pub expires_at: Option<u64>,
|
||||
pub permissions: Option<SharePermissionsDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateShareDto {
|
||||
pub password: Option<String>,
|
||||
pub expires_at: Option<u64>,
|
||||
pub permissions: Option<SharePermissionsDto>,
|
||||
}
|
||||
|
||||
/// Extension methods to convert between DTOs and domain entities
|
||||
impl ShareDto {
|
||||
pub fn from_entity(share: &Share, base_url: &str) -> Self {
|
||||
let url = format!("{}/s/{}", base_url, share.token);
|
||||
|
||||
Self {
|
||||
id: share.id.clone(),
|
||||
item_id: share.item_id.clone(),
|
||||
item_type: share.item_type.to_string(),
|
||||
token: share.token.clone(),
|
||||
url,
|
||||
has_password: share.password_hash.is_some(),
|
||||
expires_at: share.expires_at,
|
||||
permissions: SharePermissionsDto::from_entity(&share.permissions),
|
||||
created_at: share.created_at,
|
||||
created_by: share.created_by.clone(),
|
||||
access_count: share.access_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SharePermissionsDto {
|
||||
pub fn from_entity(permissions: &SharePermissions) -> Self {
|
||||
Self {
|
||||
read: permissions.read,
|
||||
write: permissions.write,
|
||||
reshare: permissions.reshare,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_entity(&self) -> SharePermissions {
|
||||
SharePermissions::new(self.read, self.write, self.reshare)
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@ pub mod outbound;
|
||||
pub mod file_ports;
|
||||
pub mod storage_ports;
|
||||
pub mod auth_ports;
|
||||
pub mod trash_ports;
|
||||
pub mod trash_ports;
|
||||
pub mod share_ports;
|
||||
@@ -0,0 +1,85 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
application::dtos::{
|
||||
pagination::PaginatedResponseDto,
|
||||
share_dto::{CreateShareDto, ShareDto, UpdateShareDto}
|
||||
},
|
||||
common::errors::DomainError,
|
||||
domain::entities::share::ShareItemType,
|
||||
};
|
||||
|
||||
|
||||
#[async_trait]
|
||||
pub trait ShareUseCase: Send + Sync + 'static {
|
||||
/// Create a new shared link for a file or folder
|
||||
async fn create_shared_link(
|
||||
&self,
|
||||
user_id: &str,
|
||||
dto: CreateShareDto,
|
||||
) -> Result<ShareDto, DomainError>;
|
||||
|
||||
/// Get a shared link by its ID
|
||||
async fn get_shared_link(&self, id: &str) -> Result<ShareDto, DomainError>;
|
||||
|
||||
/// Get a shared link by its token (for access by non-users)
|
||||
async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError>;
|
||||
|
||||
/// Get all shared links for a specific item
|
||||
async fn get_shared_links_for_item(
|
||||
&self,
|
||||
item_id: &str,
|
||||
item_type: &ShareItemType,
|
||||
) -> Result<Vec<ShareDto>, DomainError>;
|
||||
|
||||
/// Update a shared link
|
||||
async fn update_shared_link(
|
||||
&self,
|
||||
id: &str,
|
||||
dto: UpdateShareDto,
|
||||
) -> Result<ShareDto, DomainError>;
|
||||
|
||||
/// Delete a shared link
|
||||
async fn delete_shared_link(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Get all shared links created by a specific user
|
||||
async fn get_user_shared_links(
|
||||
&self,
|
||||
user_id: &str,
|
||||
page: usize,
|
||||
per_page: usize,
|
||||
) -> Result<PaginatedResponseDto<ShareDto>, DomainError>;
|
||||
|
||||
/// Verify a password for a password-protected shared link
|
||||
async fn verify_shared_link_password(
|
||||
&self,
|
||||
token: &str,
|
||||
password: &str,
|
||||
) -> Result<bool, DomainError>;
|
||||
|
||||
/// Register an access to a shared link
|
||||
async fn register_shared_link_access(&self, token: &str) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ShareStoragePort: Send + Sync + 'static {
|
||||
async fn save_share(&self, share: &crate::domain::entities::share::Share)
|
||||
-> Result<crate::domain::entities::share::Share, DomainError>;
|
||||
|
||||
async fn find_share_by_id(&self, id: &str)
|
||||
-> Result<crate::domain::entities::share::Share, DomainError>;
|
||||
|
||||
async fn find_share_by_token(&self, token: &str)
|
||||
-> Result<crate::domain::entities::share::Share, DomainError>;
|
||||
|
||||
async fn find_shares_by_item(&self, item_id: &str, item_type: &ShareItemType)
|
||||
-> Result<Vec<crate::domain::entities::share::Share>, DomainError>;
|
||||
|
||||
async fn update_share(&self, share: &crate::domain::entities::share::Share)
|
||||
-> Result<crate::domain::entities::share::Share, DomainError>;
|
||||
|
||||
async fn delete_share(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
async fn find_shares_by_user(&self, user_id: &str, offset: usize, limit: usize)
|
||||
-> Result<(Vec<crate::domain::entities::share::Share>, usize), DomainError>;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub mod file_use_case_factory;
|
||||
pub mod auth_application_service;
|
||||
pub mod trash_service;
|
||||
pub mod search_service;
|
||||
pub mod share_service;
|
||||
|
||||
#[cfg(test)]
|
||||
mod trash_service_test;
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
application::{
|
||||
dtos::{
|
||||
pagination::PaginatedResponseDto,
|
||||
share_dto::{CreateShareDto, ShareDto, SharePermissionsDto, UpdateShareDto},
|
||||
},
|
||||
ports::{
|
||||
outbound::{FileStoragePort, FolderStoragePort},
|
||||
share_ports::{ShareStoragePort, ShareUseCase},
|
||||
},
|
||||
},
|
||||
common::{config::AppConfig, errors::DomainError},
|
||||
domain::entities::share::{Share, ShareItemType, SharePermissions},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ShareServiceError {
|
||||
#[error("Share not found: {0}")]
|
||||
NotFound(String),
|
||||
#[error("Item not found: {0}")]
|
||||
ItemNotFound(String),
|
||||
#[error("Access denied: {0}")]
|
||||
AccessDenied(String),
|
||||
#[error("Invalid password: {0}")]
|
||||
InvalidPassword(String),
|
||||
#[error("Share expired")]
|
||||
Expired,
|
||||
#[error("Repository error: {0}")]
|
||||
Repository(String),
|
||||
#[error("Invalid item type: {0}")]
|
||||
InvalidItemType(String),
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
impl From<ShareServiceError> for DomainError {
|
||||
fn from(error: ShareServiceError) -> Self {
|
||||
match error {
|
||||
ShareServiceError::NotFound(s) => DomainError::not_found("Share", s),
|
||||
ShareServiceError::ItemNotFound(s) => DomainError::not_found("Item", s),
|
||||
ShareServiceError::AccessDenied(s) => DomainError::access_denied("Share", s),
|
||||
ShareServiceError::InvalidPassword(s) => DomainError::access_denied("Share", s),
|
||||
ShareServiceError::Expired => DomainError::access_denied("Share", "Share has expired".to_string()),
|
||||
ShareServiceError::Repository(s) => DomainError::internal_error("Share", s),
|
||||
ShareServiceError::InvalidItemType(s) => DomainError::validation_error("Share", s),
|
||||
ShareServiceError::Validation(s) => DomainError::validation_error("Share", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ShareService {
|
||||
config: Arc<AppConfig>,
|
||||
share_repository: Arc<dyn ShareStoragePort>,
|
||||
file_repository: Arc<dyn FileStoragePort>,
|
||||
folder_repository: Arc<dyn FolderStoragePort>,
|
||||
}
|
||||
|
||||
impl ShareService {
|
||||
pub fn new(
|
||||
config: Arc<AppConfig>,
|
||||
share_repository: Arc<dyn ShareStoragePort>,
|
||||
file_repository: Arc<dyn FileStoragePort>,
|
||||
folder_repository: Arc<dyn FolderStoragePort>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
share_repository,
|
||||
file_repository,
|
||||
folder_repository,
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifica que el elemento a compartir existe
|
||||
async fn verify_item_exists(
|
||||
&self,
|
||||
item_id: &str,
|
||||
item_type: &ShareItemType,
|
||||
) -> Result<(), ShareServiceError> {
|
||||
match item_type {
|
||||
ShareItemType::File => {
|
||||
self.file_repository
|
||||
.get_file(item_id) // Usando el método correcto del trait FileStoragePort
|
||||
.await
|
||||
.map_err(|_| ShareServiceError::ItemNotFound(format!("File with ID {} not found", item_id)))?;
|
||||
}
|
||||
ShareItemType::Folder => {
|
||||
self.folder_repository
|
||||
.get_folder(item_id) // Usando el método correcto del trait FolderStoragePort
|
||||
.await
|
||||
.map_err(|_| ShareServiceError::ItemNotFound(format!("Folder with ID {} not found", item_id)))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hash de contraseña
|
||||
fn hash_password(&self, password: &str) -> String {
|
||||
// En una implementación real, usar un algoritmo seguro como bcrypt
|
||||
// Para simplificar, solo devolvemos la misma contraseña
|
||||
password.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ShareUseCase for ShareService {
|
||||
async fn create_shared_link(
|
||||
&self,
|
||||
user_id: &str,
|
||||
dto: CreateShareDto,
|
||||
) -> Result<ShareDto, DomainError> {
|
||||
// Convertir el tipo de elemento
|
||||
let item_type = ShareItemType::try_from(dto.item_type.as_str())
|
||||
.map_err(|e| ShareServiceError::InvalidItemType(e.to_string()))?;
|
||||
|
||||
// Verificar que el elemento existe
|
||||
self.verify_item_exists(&dto.item_id, &item_type).await?;
|
||||
|
||||
// Convertir el DTO de permisos si existe
|
||||
let permissions = dto.permissions.map(|p| p.to_entity());
|
||||
|
||||
// Hash de contraseña si existe
|
||||
let password_hash = dto.password.map(|p| self.hash_password(&p));
|
||||
|
||||
// Crear la entidad Share
|
||||
let share = Share::new(
|
||||
dto.item_id.clone(),
|
||||
item_type,
|
||||
user_id.to_string(),
|
||||
permissions,
|
||||
password_hash,
|
||||
dto.expires_at,
|
||||
)
|
||||
.map_err(|e| ShareServiceError::Validation(e.to_string()))?;
|
||||
|
||||
// Guardar en el repositorio
|
||||
let saved_share = self
|
||||
.share_repository
|
||||
.save_share(&share)
|
||||
.await
|
||||
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
||||
|
||||
// Convertir la entidad a DTO para la respuesta
|
||||
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> {
|
||||
// Buscar el enlace compartido por su ID
|
||||
let share = self
|
||||
.share_repository
|
||||
.find_share_by_id(id)
|
||||
.await
|
||||
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?;
|
||||
|
||||
// Verificar si ha expirado
|
||||
if share.is_expired() {
|
||||
return Err(ShareServiceError::Expired.into());
|
||||
}
|
||||
|
||||
// Convertir la entidad a DTO para la respuesta
|
||||
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> {
|
||||
// Buscar el enlace compartido por su token
|
||||
let share = self
|
||||
.share_repository
|
||||
.find_share_by_token(token)
|
||||
.await
|
||||
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
|
||||
|
||||
// Verificar si ha expirado
|
||||
if share.is_expired() {
|
||||
return Err(ShareServiceError::Expired.into());
|
||||
}
|
||||
|
||||
// Convertir la entidad a DTO para la respuesta
|
||||
Ok(ShareDto::from_entity(&share, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
||||
}
|
||||
|
||||
async fn get_shared_links_for_item(
|
||||
&self,
|
||||
item_id: &str,
|
||||
item_type: &ShareItemType,
|
||||
) -> Result<Vec<ShareDto>, DomainError> {
|
||||
// Buscar todos los enlaces compartidos para el elemento
|
||||
let shares = self
|
||||
.share_repository
|
||||
.find_shares_by_item(item_id, item_type)
|
||||
.await
|
||||
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
||||
|
||||
// Filtrar los enlaces expirados
|
||||
let active_shares: Vec<Share> = shares.into_iter().filter(|s| !s.is_expired()).collect();
|
||||
|
||||
// Convertir las entidades a DTOs para la respuesta
|
||||
let share_dtos = active_shares
|
||||
.iter()
|
||||
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
||||
.collect();
|
||||
|
||||
Ok(share_dtos)
|
||||
}
|
||||
|
||||
async fn update_shared_link(
|
||||
&self,
|
||||
id: &str,
|
||||
dto: UpdateShareDto,
|
||||
) -> Result<ShareDto, DomainError> {
|
||||
// Buscar el enlace compartido existente
|
||||
let mut share = self
|
||||
.share_repository
|
||||
.find_share_by_id(id)
|
||||
.await
|
||||
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?;
|
||||
|
||||
// Actualizar permisos si se proporcionan
|
||||
if let Some(permissions_dto) = dto.permissions {
|
||||
let permissions = SharePermissions::new(
|
||||
permissions_dto.read,
|
||||
permissions_dto.write,
|
||||
permissions_dto.reshare,
|
||||
);
|
||||
share = share.with_permissions(permissions);
|
||||
}
|
||||
|
||||
// Actualizar contraseña si se proporciona
|
||||
if let Some(password) = dto.password {
|
||||
let password_hash = if password.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.hash_password(&password))
|
||||
};
|
||||
share = share.with_password(password_hash);
|
||||
}
|
||||
|
||||
// Actualizar fecha de expiración si se proporciona
|
||||
if dto.expires_at.is_some() {
|
||||
share = share.with_expiration(dto.expires_at);
|
||||
}
|
||||
|
||||
// Guardar los cambios
|
||||
let updated_share = self
|
||||
.share_repository
|
||||
.update_share(&share)
|
||||
.await
|
||||
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
||||
|
||||
// Convertir la entidad a DTO para la respuesta
|
||||
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> {
|
||||
// Eliminar el enlace compartido
|
||||
self.share_repository
|
||||
.delete_share(id)
|
||||
.await
|
||||
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_shared_links(
|
||||
&self,
|
||||
user_id: &str,
|
||||
page: usize,
|
||||
per_page: usize,
|
||||
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
|
||||
// Calcular offset para paginación
|
||||
let offset = (page - 1) * per_page;
|
||||
|
||||
// Buscar los enlaces compartidos del usuario
|
||||
let (shares, total) = self
|
||||
.share_repository
|
||||
.find_shares_by_user(user_id, offset, per_page)
|
||||
.await
|
||||
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
||||
|
||||
// Convertir las entidades a DTOs
|
||||
let share_dtos: Vec<ShareDto> = shares
|
||||
.iter()
|
||||
.map(|s| ShareDto::from_entity(s, &format!("http://{}:{}", self.config.server_host, self.config.server_port)))
|
||||
.collect();
|
||||
|
||||
// Crear el resultado paginado
|
||||
let paginated = PaginatedResponseDto::new(
|
||||
share_dtos,
|
||||
page,
|
||||
per_page,
|
||||
total
|
||||
);
|
||||
|
||||
Ok(paginated)
|
||||
}
|
||||
|
||||
async fn verify_shared_link_password(
|
||||
&self,
|
||||
token: &str,
|
||||
password: &str,
|
||||
) -> Result<bool, DomainError> {
|
||||
// Buscar el enlace compartido por su token
|
||||
let share = self
|
||||
.share_repository
|
||||
.find_share_by_token(token)
|
||||
.await
|
||||
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
|
||||
|
||||
// Verificar si ha expirado
|
||||
if share.is_expired() {
|
||||
return Err(ShareServiceError::Expired.into());
|
||||
}
|
||||
|
||||
// Verificar la contraseña
|
||||
Ok(share.verify_password(password))
|
||||
}
|
||||
|
||||
async fn register_shared_link_access(&self, token: &str) -> Result<(), DomainError> {
|
||||
// Buscar el enlace compartido por su token
|
||||
let share = self
|
||||
.share_repository
|
||||
.find_share_by_token(token)
|
||||
.await
|
||||
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?;
|
||||
|
||||
// Verificar si ha expirado
|
||||
if share.is_expired() {
|
||||
return Err(ShareServiceError::Expired.into());
|
||||
}
|
||||
|
||||
// Incrementar el contador de accesos
|
||||
let updated_share = share.increment_access_count();
|
||||
|
||||
// Guardar los cambios
|
||||
self.share_repository
|
||||
.update_share(&updated_share)
|
||||
.await
|
||||
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::application::ports::share_ports::ShareStoragePort;
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
struct MockFileRepository;
|
||||
struct MockFolderRepository;
|
||||
|
||||
#[async_trait]
|
||||
impl FileStoragePort for MockFileRepository {
|
||||
async fn find_file_by_id(&self, id: &str) -> Result<crate::domain::entities::file::File, DomainError> {
|
||||
if id == "test_file_id" {
|
||||
let file = crate::domain::entities::file::File::new(
|
||||
id.to_string(),
|
||||
"test.txt".to_string(),
|
||||
"/path/to/test.txt".to_string(),
|
||||
"/test.txt".to_string(),
|
||||
123,
|
||||
"text/plain".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
Ok(file)
|
||||
} else {
|
||||
Err(DomainError::NotFound(format!("File {} not found", id)))
|
||||
}
|
||||
}
|
||||
|
||||
// Implementación dummy para el resto de métodos requeridos
|
||||
async fn find_files_in_folder(&self, _folder_id: &str) -> Result<Vec<crate::domain::entities::file::File>, DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn save_file(&self, _file: &crate::domain::entities::file::File) -> Result<crate::domain::entities::file::File, DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn delete_file(&self, _id: &str) -> Result<(), DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn find_all_files(&self) -> Result<Vec<crate::domain::entities::file::File>, DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FolderStoragePort for MockFolderRepository {
|
||||
async fn find_folder_by_id(&self, id: &str) -> Result<crate::domain::entities::folder::Folder, DomainError> {
|
||||
if id == "test_folder_id" {
|
||||
let folder = crate::domain::entities::folder::Folder::new(
|
||||
id.to_string(),
|
||||
"test".to_string(),
|
||||
"/path/to/test".to_string(),
|
||||
"/test".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
Ok(folder)
|
||||
} else {
|
||||
Err(DomainError::NotFound(format!("Folder {} not found", id)))
|
||||
}
|
||||
}
|
||||
|
||||
// Implementación dummy para el resto de métodos requeridos
|
||||
async fn find_folders_in_folder(&self, _folder_id: &str) -> Result<Vec<crate::domain::entities::folder::Folder>, DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn save_folder(&self, _folder: &crate::domain::entities::folder::Folder) -> Result<crate::domain::entities::folder::Folder, DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn delete_folder(&self, _id: &str) -> Result<(), DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn find_all_folders(&self) -> Result<Vec<crate::domain::entities::folder::Folder>, DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
struct MockShareRepository {
|
||||
shares: Mutex<HashMap<String, Share>>,
|
||||
tokens: Mutex<HashMap<String, String>>, // token -> id mapping
|
||||
}
|
||||
|
||||
impl MockShareRepository {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
shares: Mutex::new(HashMap::new()),
|
||||
tokens: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ShareStoragePort for MockShareRepository {
|
||||
async fn save_share(&self, share: &Share) -> Result<Share, DomainError> {
|
||||
let mut shares = self.shares.lock().unwrap();
|
||||
let mut tokens = self.tokens.lock().unwrap();
|
||||
|
||||
shares.insert(share.id.clone(), share.clone());
|
||||
tokens.insert(share.token.clone(), share.id.clone());
|
||||
|
||||
Ok(share.clone())
|
||||
}
|
||||
|
||||
async fn find_share_by_id(&self, id: &str) -> Result<Share, DomainError> {
|
||||
let shares = self.shares.lock().unwrap();
|
||||
|
||||
shares.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| DomainError::NotFound(format!("Share with ID {} not found", id)))
|
||||
}
|
||||
|
||||
async fn find_share_by_token(&self, token: &str) -> Result<Share, DomainError> {
|
||||
let tokens = self.tokens.lock().unwrap();
|
||||
let shares = self.shares.lock().unwrap();
|
||||
|
||||
let id = tokens.get(token)
|
||||
.ok_or_else(|| DomainError::NotFound(format!("Share with token {} not found", token)))?;
|
||||
|
||||
shares.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| DomainError::NotFound(format!("Share with ID {} not found", id)))
|
||||
}
|
||||
|
||||
async fn find_shares_by_item(&self, item_id: &str, item_type: &ShareItemType) -> Result<Vec<Share>, DomainError> {
|
||||
let shares = self.shares.lock().unwrap();
|
||||
|
||||
let type_str = item_type.to_string();
|
||||
let result: Vec<Share> = shares.values()
|
||||
.filter(|s| s.item_id == item_id && s.item_type.to_string() == type_str)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn update_share(&self, share: &Share) -> Result<Share, DomainError> {
|
||||
let mut shares = self.shares.lock().unwrap();
|
||||
|
||||
if !shares.contains_key(&share.id) {
|
||||
return Err(DomainError::NotFound(format!("Share with ID {} not found for update", share.id)));
|
||||
}
|
||||
|
||||
shares.insert(share.id.clone(), share.clone());
|
||||
|
||||
Ok(share.clone())
|
||||
}
|
||||
|
||||
async fn delete_share(&self, id: &str) -> Result<(), DomainError> {
|
||||
let mut shares = self.shares.lock().unwrap();
|
||||
let mut tokens = self.tokens.lock().unwrap();
|
||||
|
||||
// Find the share to get the token
|
||||
let share = shares.get(id)
|
||||
.ok_or_else(|| DomainError::NotFound(format!("Share with ID {} not found for deletion", id)))?;
|
||||
|
||||
// Remove token mapping
|
||||
tokens.remove(&share.token);
|
||||
|
||||
// Remove the share
|
||||
shares.remove(id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_shares_by_user(&self, user_id: &str, offset: usize, limit: usize) -> Result<(Vec<Share>, usize), DomainError> {
|
||||
let shares = self.shares.lock().unwrap();
|
||||
|
||||
let user_shares: Vec<Share> = shares.values()
|
||||
.filter(|s| s.created_by == user_id)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let total = user_shares.len();
|
||||
|
||||
// Apply pagination
|
||||
let paginated = user_shares.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect();
|
||||
|
||||
Ok((paginated, total))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_shared_link() {
|
||||
let config = Arc::new(Config {
|
||||
base_url: "http://localhost:8085".to_string(),
|
||||
storage_path: "/tmp/storage".to_string(),
|
||||
log_level: "info".to_string(),
|
||||
port: 8085,
|
||||
database_url: "".to_string(),
|
||||
jwt_secret: "test_secret".to_string(),
|
||||
jwt_expiration: 3600,
|
||||
enable_cors: false,
|
||||
cors_origins: vec![],
|
||||
});
|
||||
|
||||
let share_repo = Arc::new(MockShareRepository::new());
|
||||
let file_repo = Arc::new(MockFileRepository);
|
||||
let folder_repo = Arc::new(MockFolderRepository);
|
||||
|
||||
let service = ShareService::new(config, share_repo, file_repo, folder_repo);
|
||||
|
||||
// Test creating a file share
|
||||
let dto = CreateShareDto {
|
||||
item_id: "test_file_id".to_string(),
|
||||
item_type: "file".to_string(),
|
||||
password: Some("secret".to_string()),
|
||||
expires_at: None,
|
||||
permissions: Some(SharePermissionsDto {
|
||||
read: true,
|
||||
write: false,
|
||||
reshare: false,
|
||||
}),
|
||||
};
|
||||
|
||||
let result = service.create_shared_link("user123", dto).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let share_dto = result.unwrap();
|
||||
assert_eq!(share_dto.item_id, "test_file_id");
|
||||
assert_eq!(share_dto.item_type, "file");
|
||||
assert!(share_dto.has_password);
|
||||
assert!(share_dto.url.starts_with("http://localhost:8085/s/"));
|
||||
}
|
||||
}
|
||||
+22
-1
@@ -281,7 +281,7 @@ impl Default for FeaturesConfig {
|
||||
Self {
|
||||
enable_auth: true, // Enable authentication by default
|
||||
enable_user_storage_quotas: false,
|
||||
enable_file_sharing: false,
|
||||
enable_file_sharing: true, // Enable file sharing by default
|
||||
enable_trash: true, // Enable trash feature
|
||||
enable_search: true, // Enable search feature
|
||||
}
|
||||
@@ -412,6 +412,27 @@ impl AppConfig {
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(enable_file_sharing) = env::var("OXICLOUD_ENABLE_FILE_SHARING")
|
||||
.map(|v| v.parse::<bool>()) {
|
||||
if let Ok(val) = enable_file_sharing {
|
||||
config.features.enable_file_sharing = val;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(enable_trash) = env::var("OXICLOUD_ENABLE_TRASH")
|
||||
.map(|v| v.parse::<bool>()) {
|
||||
if let Ok(val) = enable_trash {
|
||||
config.features.enable_trash = val;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(enable_search) = env::var("OXICLOUD_ENABLE_SEARCH")
|
||||
.map(|v| v.parse::<bool>()) {
|
||||
if let Ok(val) = enable_search {
|
||||
config.features.enable_search = val;
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
|
||||
@@ -239,6 +239,7 @@ impl AppServiceFactory {
|
||||
i18n_service,
|
||||
trash_service,
|
||||
search_service,
|
||||
share_service: None, // No share service by default
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,6 +282,7 @@ pub struct ApplicationServices {
|
||||
pub i18n_service: Arc<I18nApplicationService>,
|
||||
pub trash_service: Option<Arc<dyn TrashUseCase>>,
|
||||
pub search_service: Option<Arc<dyn SearchUseCase>>,
|
||||
pub share_service: Option<Arc<dyn crate::application::ports::share_ports::ShareUseCase>>,
|
||||
}
|
||||
|
||||
/// Contenedor para servicios de autenticación
|
||||
@@ -300,6 +302,7 @@ pub struct AppState {
|
||||
pub db_pool: Option<Arc<PgPool>>,
|
||||
pub auth_service: Option<AuthServices>,
|
||||
pub trash_service: Option<Arc<dyn TrashUseCase>>,
|
||||
pub share_service: Option<Arc<dyn crate::application::ports::share_ports::ShareUseCase>>,
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
@@ -768,6 +771,7 @@ impl Default for AppState {
|
||||
i18n_service: Arc::new(DummyI18nApplicationService::dummy()),
|
||||
trash_service: None, // No trash service in minimal mode
|
||||
search_service: Some(Arc::new(DummySearchUseCase) as Arc<dyn crate::application::ports::inbound::SearchUseCase>),
|
||||
share_service: None, // No share service in minimal mode
|
||||
};
|
||||
|
||||
// Return a minimal app state
|
||||
@@ -778,6 +782,7 @@ impl Default for AppState {
|
||||
db_pool: None,
|
||||
auth_service: None,
|
||||
trash_service: None,
|
||||
share_service: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,6 +800,7 @@ impl AppState {
|
||||
db_pool: None,
|
||||
auth_service: None,
|
||||
trash_service: None,
|
||||
share_service: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,4 +818,9 @@ impl AppState {
|
||||
self.trash_service = Some(trash_service);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_share_service(mut self, share_service: Arc<dyn crate::application::ports::share_ports::ShareUseCase>) -> Self {
|
||||
self.share_service = Some(share_service);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@ pub mod file;
|
||||
pub mod folder;
|
||||
pub mod user;
|
||||
pub mod session;
|
||||
pub mod share;
|
||||
pub mod trashed_item;
|
||||
@@ -0,0 +1,244 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Share {
|
||||
pub id: String,
|
||||
pub item_id: String,
|
||||
pub item_type: ShareItemType,
|
||||
pub token: String,
|
||||
pub password_hash: Option<String>,
|
||||
pub expires_at: Option<u64>,
|
||||
pub permissions: SharePermissions,
|
||||
pub created_at: u64,
|
||||
pub created_by: String,
|
||||
pub access_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SharePermissions {
|
||||
pub read: bool,
|
||||
pub write: bool,
|
||||
pub reshare: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ShareItemType {
|
||||
File,
|
||||
Folder,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ShareError {
|
||||
#[error("Invalid token: {0}")]
|
||||
InvalidToken(String),
|
||||
#[error("Invalid expiration date: {0}")]
|
||||
InvalidExpiration(String),
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
impl Share {
|
||||
pub fn new(
|
||||
item_id: String,
|
||||
item_type: ShareItemType,
|
||||
created_by: String,
|
||||
permissions: Option<SharePermissions>,
|
||||
password_hash: Option<String>,
|
||||
expires_at: Option<u64>,
|
||||
) -> Result<Self, ShareError> {
|
||||
// Validate item_id
|
||||
if item_id.is_empty() {
|
||||
return Err(ShareError::ValidationError("Item ID cannot be empty".to_string()));
|
||||
}
|
||||
|
||||
// Validate expiration date if provided
|
||||
if let Some(expires) = expires_at {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
|
||||
if expires <= now {
|
||||
return Err(ShareError::InvalidExpiration("Expiration date must be in the future".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
|
||||
Ok(Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
item_id,
|
||||
item_type,
|
||||
token: Uuid::new_v4().to_string(),
|
||||
password_hash,
|
||||
expires_at,
|
||||
permissions: permissions.unwrap_or(SharePermissions {
|
||||
read: true,
|
||||
write: false,
|
||||
reshare: false,
|
||||
}),
|
||||
created_at: now,
|
||||
created_by,
|
||||
access_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_permissions(mut self, permissions: SharePermissions) -> Self {
|
||||
self.permissions = permissions;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_password(mut self, password_hash: Option<String>) -> Self {
|
||||
self.password_hash = password_hash;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_expiration(mut self, expires_at: Option<u64>) -> Self {
|
||||
self.expires_at = expires_at;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_token(mut self, token: String) -> Self {
|
||||
self.token = token;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
if let Some(expires_at) = self.expires_at {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
|
||||
return expires_at <= now;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn increment_access_count(mut self) -> Self {
|
||||
self.access_count += 1;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn verify_password(&self, password: &str) -> bool {
|
||||
match &self.password_hash {
|
||||
Some(hash) => {
|
||||
// In a real implementation, use a proper password hashing function like bcrypt
|
||||
// For simplicity, we're just comparing strings here
|
||||
hash == password
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SharePermissions {
|
||||
pub fn new(read: bool, write: bool, reshare: bool) -> Self {
|
||||
Self {
|
||||
read,
|
||||
write,
|
||||
reshare,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for ShareItemType {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
ShareItemType::File => "file".to_string(),
|
||||
ShareItemType::Folder => "folder".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ShareItemType {
|
||||
type Error = ShareError;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"file" => Ok(ShareItemType::File),
|
||||
"folder" => Ok(ShareItemType::Folder),
|
||||
_ => Err(ShareError::ValidationError(format!("Invalid item type: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_share() {
|
||||
let share = Share::new(
|
||||
"test_file_id".to_string(),
|
||||
ShareItemType::File,
|
||||
"user123".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(share.item_id, "test_file_id");
|
||||
assert_eq!(share.item_type, ShareItemType::File);
|
||||
assert_eq!(share.created_by, "user123");
|
||||
assert_eq!(share.permissions.read, true);
|
||||
assert_eq!(share.permissions.write, false);
|
||||
assert_eq!(share.permissions.reshare, false);
|
||||
assert!(share.password_hash.is_none());
|
||||
assert!(share.expires_at.is_none());
|
||||
assert_eq!(share.access_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_share_is_expired() {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
|
||||
// Create a share that expires in the future
|
||||
let future = now + 3600; // 1 hour in the future
|
||||
let share = Share::new(
|
||||
"test_file_id".to_string(),
|
||||
ShareItemType::File,
|
||||
"user123".to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(future),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(!share.is_expired());
|
||||
|
||||
// Test with past expiration (should fail during creation)
|
||||
let past = now - 3600; // 1 hour in the past
|
||||
let share_result = Share::new(
|
||||
"test_file_id".to_string(),
|
||||
ShareItemType::File,
|
||||
"user123".to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(past),
|
||||
);
|
||||
|
||||
assert!(share_result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_share_item_type_conversion() {
|
||||
assert_eq!(ShareItemType::File.to_string(), "file");
|
||||
assert_eq!(ShareItemType::Folder.to_string(), "folder");
|
||||
|
||||
assert_eq!(ShareItemType::try_from("file").unwrap(), ShareItemType::File);
|
||||
assert_eq!(ShareItemType::try_from("folder").unwrap(), ShareItemType::Folder);
|
||||
assert_eq!(ShareItemType::try_from("FILE").unwrap(), ShareItemType::File);
|
||||
assert!(ShareItemType::try_from("invalid").is_err());
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@ pub mod file_repository;
|
||||
pub mod folder_repository;
|
||||
pub mod user_repository;
|
||||
pub mod session_repository;
|
||||
pub mod share_repository;
|
||||
pub mod trash_repository;
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::domain::{
|
||||
entities::share::{Share, ShareItemType},
|
||||
repositories::user_repository::UserRepositoryError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ShareRepositoryError {
|
||||
#[error("Share not found: {0}")]
|
||||
NotFound(String),
|
||||
#[error("Item not found: {0}")]
|
||||
ItemNotFound(String),
|
||||
#[error("Storage error: {0}")]
|
||||
StorageError(String),
|
||||
#[error("User repository error: {0}")]
|
||||
UserRepository(#[from] UserRepositoryError),
|
||||
#[error("Share already exists: {0}")]
|
||||
AlreadyExists(String),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ShareRepository: Send + Sync + 'static {
|
||||
/// Save a new share or update an existing one
|
||||
async fn save(&self, share: &Share) -> Result<Share, ShareRepositoryError>;
|
||||
|
||||
/// Find a share by its ID
|
||||
async fn find_by_id(&self, id: &str) -> Result<Share, ShareRepositoryError>;
|
||||
|
||||
/// Find a share by its token
|
||||
async fn find_by_token(&self, token: &str) -> Result<Share, ShareRepositoryError>;
|
||||
|
||||
/// Find all shares for a specific item
|
||||
async fn find_by_item(&self, item_id: &str, item_type: &ShareItemType) -> Result<Vec<Share>, ShareRepositoryError>;
|
||||
|
||||
/// Delete a share by its ID
|
||||
async fn delete(&self, id: &str) -> Result<(), ShareRepositoryError>;
|
||||
|
||||
/// Find all shares created by a specific user
|
||||
async fn find_by_user(&self, user_id: &str) -> Result<Vec<Share>, ShareRepositoryError>;
|
||||
|
||||
/// Find all shares (admin operation)
|
||||
async fn find_all(&self) -> Result<Vec<Share>, ShareRepositoryError>;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub mod file_fs_write_repository;
|
||||
pub mod trash_fs_repository;
|
||||
pub mod file_fs_repository_trash;
|
||||
pub mod folder_fs_repository_trash;
|
||||
pub mod share_fs_repository;
|
||||
|
||||
// Repositorios PostgreSQL
|
||||
pub mod pg;
|
||||
@@ -19,4 +20,5 @@ pub use file_metadata_manager::FileMetadataManager;
|
||||
pub use file_path_resolver::FilePathResolver;
|
||||
pub use file_fs_read_repository::FileFsReadRepository;
|
||||
pub use file_fs_write_repository::FileFsWriteRepository;
|
||||
pub use pg::{UserPgRepository, SessionPgRepository};
|
||||
pub use pg::{UserPgRepository, SessionPgRepository};
|
||||
pub use share_fs_repository::ShareFsRepository;
|
||||
@@ -0,0 +1,250 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{fs, io};
|
||||
|
||||
use crate::{
|
||||
application::ports::share_ports::ShareStoragePort,
|
||||
common::{config::AppConfig, errors::DomainError},
|
||||
domain::{
|
||||
entities::share::{Share, ShareItemType},
|
||||
},
|
||||
};
|
||||
|
||||
// Estructura para almacenar en el sistema de archivos
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ShareRecord {
|
||||
id: String,
|
||||
item_id: String,
|
||||
item_type: String,
|
||||
token: String,
|
||||
password_hash: Option<String>,
|
||||
expires_at: Option<u64>,
|
||||
permissions_read: bool,
|
||||
permissions_write: bool,
|
||||
permissions_reshare: bool,
|
||||
created_at: u64,
|
||||
created_by: String,
|
||||
access_count: u64,
|
||||
}
|
||||
|
||||
pub struct ShareFsRepository {
|
||||
config: Arc<AppConfig>,
|
||||
}
|
||||
|
||||
impl ShareFsRepository {
|
||||
pub fn new(config: Arc<AppConfig>) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
/// Obtiene la ruta del archivo JSON donde se almacenan los enlaces compartidos
|
||||
fn get_shares_path(&self) -> String {
|
||||
format!("{}/shares.json", self.config.storage_path.display())
|
||||
}
|
||||
|
||||
/// Lee todos los enlaces compartidos del archivo JSON
|
||||
async fn read_shares(&self) -> Result<Vec<ShareRecord>, io::Error> {
|
||||
let path = self.get_shares_path();
|
||||
let path = Path::new(&path);
|
||||
|
||||
if !path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(path).await?;
|
||||
let shares: Vec<ShareRecord> = serde_json::from_str(&content).unwrap_or_default();
|
||||
|
||||
Ok(shares)
|
||||
}
|
||||
|
||||
/// Guarda todos los enlaces compartidos en el archivo JSON
|
||||
async fn write_shares(&self, shares: &[ShareRecord]) -> Result<(), io::Error> {
|
||||
let path = self.get_shares_path();
|
||||
let json = serde_json::to_string_pretty(shares)?;
|
||||
|
||||
// Asegúrate de que el directorio existe
|
||||
let dir = Path::new(&path).parent().unwrap();
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(dir).await?
|
||||
}
|
||||
|
||||
fs::write(path, json).await
|
||||
}
|
||||
|
||||
/// Convierte un registro del sistema de archivos a una entidad de dominio
|
||||
fn to_entity(&self, record: &ShareRecord) -> Share {
|
||||
let item_type = ShareItemType::try_from(record.item_type.as_str())
|
||||
.unwrap_or(ShareItemType::File);
|
||||
|
||||
let permissions = crate::domain::entities::share::SharePermissions::new(
|
||||
record.permissions_read,
|
||||
record.permissions_write,
|
||||
record.permissions_reshare,
|
||||
);
|
||||
|
||||
Share {
|
||||
id: record.id.clone(),
|
||||
item_id: record.item_id.clone(),
|
||||
item_type,
|
||||
token: record.token.clone(),
|
||||
password_hash: record.password_hash.clone(),
|
||||
expires_at: record.expires_at,
|
||||
permissions,
|
||||
created_at: record.created_at,
|
||||
created_by: record.created_by.clone(),
|
||||
access_count: record.access_count,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convierte una entidad de dominio a un registro para el sistema de archivos
|
||||
fn to_record(&self, share: &Share) -> ShareRecord {
|
||||
ShareRecord {
|
||||
id: share.id.clone(),
|
||||
item_id: share.item_id.clone(),
|
||||
item_type: share.item_type.to_string(),
|
||||
token: share.token.clone(),
|
||||
password_hash: share.password_hash.clone(),
|
||||
expires_at: share.expires_at,
|
||||
permissions_read: share.permissions.read,
|
||||
permissions_write: share.permissions.write,
|
||||
permissions_reshare: share.permissions.reshare,
|
||||
created_at: share.created_at,
|
||||
created_by: share.created_by.clone(),
|
||||
access_count: share.access_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ShareStoragePort for ShareFsRepository {
|
||||
async fn save_share(&self, share: &Share) -> Result<Share, DomainError> {
|
||||
let mut shares = self.read_shares().await
|
||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||
|
||||
// Verifica si el enlace ya existe
|
||||
let existing_index = shares.iter().position(|s| s.id == share.id);
|
||||
|
||||
let record = self.to_record(share);
|
||||
|
||||
if let Some(index) = existing_index {
|
||||
// Actualización
|
||||
shares[index] = record;
|
||||
} else {
|
||||
// Inserción
|
||||
shares.push(record);
|
||||
}
|
||||
|
||||
self.write_shares(&shares).await
|
||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||
|
||||
Ok(share.clone())
|
||||
}
|
||||
|
||||
async fn find_share_by_id(&self, id: &str) -> Result<Share, DomainError> {
|
||||
let shares = self.read_shares().await
|
||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||
|
||||
let share = shares.iter()
|
||||
.find(|s| s.id == id)
|
||||
.ok_or_else(|| {
|
||||
DomainError::not_found("Share", format!("Share with ID {} not found", id))
|
||||
});
|
||||
|
||||
match share {
|
||||
Ok(record) => Ok(self.to_entity(record)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_share_by_token(&self, token: &str) -> Result<Share, DomainError> {
|
||||
let shares = self.read_shares().await
|
||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||
|
||||
let share = shares.iter()
|
||||
.find(|s| s.token == token)
|
||||
.ok_or_else(|| {
|
||||
DomainError::not_found("Share", format!("Share with token {} not found", token))
|
||||
});
|
||||
|
||||
match share {
|
||||
Ok(record) => Ok(self.to_entity(record)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_shares_by_item(&self, item_id: &str, item_type: &ShareItemType) -> Result<Vec<Share>, DomainError> {
|
||||
let shares = self.read_shares().await
|
||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||
|
||||
let type_str = item_type.to_string();
|
||||
let result: Vec<Share> = shares.iter()
|
||||
.filter(|s| s.item_id == item_id && s.item_type == type_str)
|
||||
.map(|record| self.to_entity(record))
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn update_share(&self, share: &Share) -> Result<Share, DomainError> {
|
||||
let mut shares = self.read_shares().await
|
||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||
|
||||
// Busca el índice del enlace a actualizar
|
||||
let index = shares.iter().position(|s| s.id == share.id)
|
||||
.ok_or_else(|| {
|
||||
DomainError::not_found("Share", format!("Share with ID {} not found for update", share.id))
|
||||
})?;
|
||||
|
||||
// Actualiza el registro
|
||||
shares[index] = self.to_record(share);
|
||||
|
||||
// Guarda los cambios
|
||||
self.write_shares(&shares).await
|
||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||
|
||||
Ok(share.clone())
|
||||
}
|
||||
|
||||
async fn delete_share(&self, id: &str) -> Result<(), DomainError> {
|
||||
let mut shares = self.read_shares().await
|
||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||
|
||||
// Encuentra el índice del enlace a eliminar
|
||||
let initial_len = shares.len();
|
||||
shares.retain(|s| s.id != id);
|
||||
|
||||
// Si no se eliminó ningún enlace, significa que no existía
|
||||
if shares.len() == initial_len {
|
||||
return Err(DomainError::not_found("Share", format!("Share with ID {} not found for deletion", id)));
|
||||
}
|
||||
|
||||
// Guarda los cambios
|
||||
self.write_shares(&shares).await
|
||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_shares_by_user(&self, user_id: &str, offset: usize, limit: usize) -> Result<(Vec<Share>, usize), DomainError> {
|
||||
let shares = self.read_shares().await
|
||||
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
|
||||
|
||||
// Filtra los enlaces del usuario
|
||||
let user_shares: Vec<ShareRecord> = shares.into_iter()
|
||||
.filter(|s| s.created_by == user_id)
|
||||
.collect();
|
||||
|
||||
// Calcula el total
|
||||
let total = user_shares.len();
|
||||
|
||||
// Aplica la paginación
|
||||
let paginated: Vec<Share> = user_shares.iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.map(|record| self.to_entity(record))
|
||||
.collect();
|
||||
|
||||
Ok((paginated, total))
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod batch_handler;
|
||||
pub mod auth_handler;
|
||||
pub mod trash_handler;
|
||||
pub mod search_handler;
|
||||
pub mod share_handler;
|
||||
|
||||
/// Tipo de resultado para controladores de API
|
||||
pub type ApiResult<T> = Result<T, (axum::http::StatusCode, String)>;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
application::{
|
||||
dtos::share_dto::{CreateShareDto, UpdateShareDto},
|
||||
ports::share_ports::ShareUseCase
|
||||
},
|
||||
common::errors::{DomainError, ErrorKind},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GetSharesQuery {
|
||||
pub page: Option<usize>,
|
||||
pub per_page: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VerifyPasswordRequest {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// Create a new shared link
|
||||
pub async fn create_shared_link(
|
||||
State(share_use_case): State<Arc<dyn ShareUseCase>>,
|
||||
Json(dto): Json<CreateShareDto>,
|
||||
) -> impl IntoResponse {
|
||||
// For now, we'll use a default user ID until auth is implemented
|
||||
let user_id = "default-user";
|
||||
match share_use_case.create_shared_link(&user_id, dto).await {
|
||||
Ok(share) => (StatusCode::CREATED, Json(share)).into_response(),
|
||||
Err(err) => {
|
||||
let status = match err.kind {
|
||||
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||
ErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(status, Json(json!({ "error": err.to_string() }))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get information about a specific shared link by ID
|
||||
pub async fn get_shared_link(
|
||||
State(share_use_case): State<Arc<dyn ShareUseCase>>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match share_use_case.get_shared_link(&id).await {
|
||||
Ok(share) => (StatusCode::OK, Json(share)).into_response(),
|
||||
Err(err) => {
|
||||
let status = match err.kind {
|
||||
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(status, Json(json!({ "error": err.to_string() }))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all shared links created by the current user
|
||||
pub async fn get_user_shares(
|
||||
State(share_use_case): State<Arc<dyn ShareUseCase>>,
|
||||
Query(query): Query<GetSharesQuery>,
|
||||
) -> impl IntoResponse {
|
||||
// For now, we'll use a default user ID until auth is implemented
|
||||
let user_id = "default-user";
|
||||
let page = query.page.unwrap_or(1);
|
||||
let per_page = query.per_page.unwrap_or(20);
|
||||
|
||||
match share_use_case.get_user_shared_links(&user_id, page, per_page).await {
|
||||
Ok(shares) => (StatusCode::OK, Json(shares)).into_response(),
|
||||
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": err.to_string() }))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a shared link's properties
|
||||
pub async fn update_shared_link(
|
||||
State(share_use_case): State<Arc<dyn ShareUseCase>>,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<UpdateShareDto>,
|
||||
) -> impl IntoResponse {
|
||||
match share_use_case.update_shared_link(&id, dto).await {
|
||||
Ok(share) => (StatusCode::OK, Json(share)).into_response(),
|
||||
Err(err) => {
|
||||
let status = match err.kind {
|
||||
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||
ErrorKind::AccessDenied => StatusCode::FORBIDDEN,
|
||||
ErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(status, Json(json!({ "error": err.to_string() }))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a shared link
|
||||
pub async fn delete_shared_link(
|
||||
State(share_use_case): State<Arc<dyn ShareUseCase>>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match share_use_case.delete_shared_link(&id).await {
|
||||
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => {
|
||||
let status = match err.kind {
|
||||
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||
ErrorKind::AccessDenied => StatusCode::FORBIDDEN,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(status, Json(json!({ "error": err.to_string() }))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Access a shared item via its token
|
||||
pub async fn access_shared_item(
|
||||
State(share_use_case): State<Arc<dyn ShareUseCase>>,
|
||||
Path(token): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
// Register the access
|
||||
let _ = share_use_case.register_shared_link_access(&token).await;
|
||||
|
||||
// Get the shared link
|
||||
match share_use_case.get_shared_link_by_token(&token).await {
|
||||
Ok(item) => (StatusCode::OK, Json(item)).into_response(),
|
||||
Err(err) => {
|
||||
let status = match err.kind {
|
||||
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||
ErrorKind::AccessDenied => {
|
||||
if err.message.contains("expired") {
|
||||
StatusCode::GONE // HTTP 410 Gone for expired links
|
||||
} else if err.message.contains("password") {
|
||||
return (StatusCode::UNAUTHORIZED, Json(json!({
|
||||
"error": "Password required",
|
||||
"requiresPassword": true
|
||||
}))).into_response();
|
||||
} else {
|
||||
StatusCode::FORBIDDEN
|
||||
}
|
||||
},
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
(status, Json(json!({ "error": err.to_string() }))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify password for a password-protected shared item
|
||||
pub async fn verify_shared_item_password(
|
||||
State(share_use_case): State<Arc<dyn ShareUseCase>>,
|
||||
Path(token): Path<String>,
|
||||
Json(req): Json<VerifyPasswordRequest>,
|
||||
) -> impl IntoResponse {
|
||||
match share_use_case.verify_shared_link_password(&token, &req.password).await {
|
||||
Ok(item) => (StatusCode::OK, Json(item)).into_response(),
|
||||
Err(err) => {
|
||||
let status = match err.kind {
|
||||
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||
ErrorKind::AccessDenied => {
|
||||
if err.message.contains("expired") {
|
||||
StatusCode::GONE
|
||||
} else if err.message.contains("password") {
|
||||
StatusCode::UNAUTHORIZED
|
||||
} else {
|
||||
StatusCode::FORBIDDEN
|
||||
}
|
||||
},
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(status, Json(json!({ "error": err.to_string() }))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,12 @@ use crate::application::services::i18n_application_service::I18nApplicationServi
|
||||
use crate::application::services::batch_operations::BatchOperationService;
|
||||
use crate::application::ports::trash_ports::TrashUseCase;
|
||||
use crate::application::ports::inbound::SearchUseCase;
|
||||
use crate::application::ports::share_ports::ShareUseCase;
|
||||
|
||||
use crate::interfaces::api::handlers::folder_handler::FolderHandler;
|
||||
use crate::interfaces::api::handlers::file_handler::FileHandler;
|
||||
use crate::interfaces::api::handlers::i18n_handler::I18nHandler;
|
||||
// Eliminamos la importación de ShareHandler ya que ahora usamos directamente el servicio
|
||||
use crate::interfaces::api::handlers::batch_handler::{
|
||||
self, BatchHandlerState
|
||||
};
|
||||
@@ -40,6 +42,7 @@ pub fn create_api_routes(
|
||||
i18n_service: Option<Arc<I18nApplicationService>>,
|
||||
trash_service: Option<Arc<dyn TrashUseCase>>,
|
||||
search_service: Option<Arc<dyn SearchUseCase>>,
|
||||
share_service: Option<Arc<dyn ShareUseCase>>,
|
||||
) -> Router<crate::common::di::AppState> {
|
||||
// Create a simplified AppState for the trash view
|
||||
// Setup required components for repository construction
|
||||
@@ -72,7 +75,7 @@ pub fn create_api_routes(
|
||||
path_service.clone(),
|
||||
));
|
||||
|
||||
let app_state = crate::common::di::AppState {
|
||||
let mut app_state = crate::common::di::AppState {
|
||||
core: crate::common::di::CoreServices {
|
||||
path_service: path_service.clone(),
|
||||
cache_manager: Arc::new(crate::infrastructure::services::cache_manager::StorageCacheManager::default()),
|
||||
@@ -102,10 +105,12 @@ pub fn create_api_routes(
|
||||
),
|
||||
trash_service: trash_service.clone(), // Include the trash service here too for consistency
|
||||
search_service: search_service.clone(), // Include the search service
|
||||
share_service: share_service.clone(), // Include the share service
|
||||
},
|
||||
db_pool: None,
|
||||
auth_service: None,
|
||||
trash_service: trash_service.clone(), // This is the important part - include the trash service
|
||||
share_service: share_service.clone() // Include the share service for routes
|
||||
};
|
||||
// Inicializar el servicio de operaciones por lotes
|
||||
let batch_service = Arc::new(BatchOperationService::default(
|
||||
@@ -284,12 +289,49 @@ pub fn create_api_routes(
|
||||
Router::new()
|
||||
};
|
||||
|
||||
// Implementaciones directas de handlers para compartir, sin depender de ShareHandler
|
||||
|
||||
// Create routes for shared resources if the service is available
|
||||
let share_router = if let Some(share_service) = share_service.clone() {
|
||||
use crate::interfaces::api::handlers::share_handler;
|
||||
|
||||
Router::new()
|
||||
.route("/", post(share_handler::create_shared_link))
|
||||
.route("/", get(share_handler::get_user_shares))
|
||||
.route("/{id}", get(share_handler::get_shared_link))
|
||||
.route("/{id}", put(share_handler::update_shared_link))
|
||||
.route("/{id}", delete(share_handler::delete_shared_link))
|
||||
.with_state(share_service.clone())
|
||||
} else {
|
||||
Router::new()
|
||||
};
|
||||
|
||||
// Public route for accessing shared links
|
||||
let public_share_router = if let Some(share_service) = share_service.clone() {
|
||||
use crate::interfaces::api::handlers::share_handler;
|
||||
|
||||
Router::new()
|
||||
.route("/{token}", get(share_handler::access_shared_item))
|
||||
.route("/{token}/verify", post(share_handler::verify_shared_item_password))
|
||||
.with_state(share_service.clone())
|
||||
} else {
|
||||
Router::new()
|
||||
};
|
||||
|
||||
// Create a router without the i18n routes
|
||||
let mut router = Router::new()
|
||||
.nest("/folders", folders_router)
|
||||
.nest("/files", files_router)
|
||||
.nest("/batch", batch_router)
|
||||
.nest("/search", search_router);
|
||||
.nest("/search", search_router)
|
||||
.nest("/shares", share_router)
|
||||
.nest("/s", public_share_router)
|
||||
;
|
||||
|
||||
// Store the share service in app_state for future use
|
||||
if let Some(share_service) = share_service.clone() {
|
||||
app_state.share_service = Some(share_service);
|
||||
}
|
||||
|
||||
// Re-enable trash routes to make the trash view work
|
||||
if let Some(_trash_service_ref) = trash_service.clone() {
|
||||
|
||||
+24
-1
@@ -45,10 +45,12 @@ use application::services::folder_service::FolderService;
|
||||
use application::services::file_service::FileService;
|
||||
use application::services::i18n_application_service::I18nApplicationService;
|
||||
use application::services::storage_mediator::FileSystemStorageMediator;
|
||||
use application::services::share_service::ShareService;
|
||||
use domain::services::path_service::PathService;
|
||||
use infrastructure::repositories::folder_fs_repository::FolderFsRepository;
|
||||
use infrastructure::repositories::file_fs_repository::FileFsRepository;
|
||||
use infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||
use infrastructure::repositories::share_fs_repository::ShareFsRepository;
|
||||
use infrastructure::services::file_system_i18n_service::FileSystemI18nService;
|
||||
use infrastructure::services::id_mapping_service::IdMappingService;
|
||||
use infrastructure::services::id_mapping_optimizer::IdMappingOptimizer;
|
||||
@@ -591,6 +593,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing::info!("Search service initialized with caching (TTL: 300s, max entries: 1000)");
|
||||
Some(search_service)
|
||||
};
|
||||
|
||||
// Initialize share repository and service if enabled
|
||||
let share_service: Option<Arc<dyn application::ports::share_ports::ShareUseCase>> = if config.features.enable_file_sharing {
|
||||
let share_repository = Arc::new(ShareFsRepository::new(
|
||||
Arc::new(config.clone())
|
||||
));
|
||||
|
||||
let share_service = Arc::new(ShareService::new(
|
||||
Arc::new(config.clone()),
|
||||
share_repository,
|
||||
file_repository.clone(),
|
||||
folder_repository.clone()
|
||||
));
|
||||
|
||||
tracing::info!("File sharing service initialized successfully");
|
||||
Some(share_service)
|
||||
} else {
|
||||
tracing::info!("File sharing service is disabled in configuration");
|
||||
None
|
||||
};
|
||||
|
||||
let application_services = common::di::ApplicationServices {
|
||||
folder_service: folder_service.clone(),
|
||||
@@ -602,6 +624,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
i18n_service: i18n_service.clone(),
|
||||
trash_service: trash_service.clone(),
|
||||
search_service: search_service.clone(),
|
||||
share_service: share_service.clone(),
|
||||
};
|
||||
|
||||
// Create the AppState without Arc first
|
||||
@@ -626,7 +649,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let app_state = Arc::new(app_state);
|
||||
|
||||
// Build application router
|
||||
let api_routes = create_api_routes(folder_service, file_service, Some(i18n_service), trash_service, search_service);
|
||||
let api_routes = create_api_routes(folder_service, file_service, Some(i18n_service), trash_service, search_service, share_service);
|
||||
let web_routes = create_web_routes();
|
||||
|
||||
// Build the app router
|
||||
|
||||
Reference in New Issue
Block a user