refactoring hexagonal and clean architecture
This commit is contained in:
@@ -58,12 +58,12 @@ impl ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
// Check if user is owner
|
||||
if address_book.owner_id == user_id {
|
||||
if address_book.owner_id() == user_id {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
// Check if address book is public
|
||||
if address_book.is_public {
|
||||
if address_book.is_public() {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ impl ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
// Owner always has write access
|
||||
if address_book.owner_id == user_id {
|
||||
if address_book.owner_id() == user_id {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
@@ -132,41 +132,41 @@ impl ContactStorageAdapter {
|
||||
fn generate_vcard(contact: &Contact) -> String {
|
||||
let mut vcard = String::from("BEGIN:VCARD\nVERSION:3.0\n");
|
||||
|
||||
if let Some(ref full_name) = contact.full_name {
|
||||
if let Some(full_name) = contact.full_name() {
|
||||
vcard.push_str(&format!("FN:{}\n", full_name));
|
||||
}
|
||||
|
||||
if contact.first_name.is_some() || contact.last_name.is_some() {
|
||||
let last = contact.last_name.as_deref().unwrap_or("");
|
||||
let first = contact.first_name.as_deref().unwrap_or("");
|
||||
if contact.first_name().is_some() || contact.last_name().is_some() {
|
||||
let last = contact.last_name().unwrap_or("");
|
||||
let first = contact.first_name().unwrap_or("");
|
||||
vcard.push_str(&format!("N:{};{};;;\n", last, first));
|
||||
}
|
||||
|
||||
if let Some(ref nickname) = contact.nickname {
|
||||
if let Some(nickname) = contact.nickname() {
|
||||
vcard.push_str(&format!("NICKNAME:{}\n", nickname));
|
||||
}
|
||||
|
||||
for email in &contact.email {
|
||||
for email in contact.email() {
|
||||
vcard.push_str(&format!("EMAIL;TYPE={}:{}\n", email.r#type.to_uppercase(), email.email));
|
||||
}
|
||||
|
||||
for phone in &contact.phone {
|
||||
for phone in contact.phone() {
|
||||
vcard.push_str(&format!("TEL;TYPE={}:{}\n", phone.r#type.to_uppercase(), phone.number));
|
||||
}
|
||||
|
||||
if let Some(ref org) = contact.organization {
|
||||
if let Some(org) = contact.organization() {
|
||||
vcard.push_str(&format!("ORG:{}\n", org));
|
||||
}
|
||||
|
||||
if let Some(ref title) = contact.title {
|
||||
if let Some(title) = contact.title() {
|
||||
vcard.push_str(&format!("TITLE:{}\n", title));
|
||||
}
|
||||
|
||||
if let Some(ref notes) = contact.notes {
|
||||
if let Some(notes) = contact.notes() {
|
||||
vcard.push_str(&format!("NOTE:{}\n", notes));
|
||||
}
|
||||
|
||||
vcard.push_str(&format!("UID:{}\n", contact.uid));
|
||||
vcard.push_str(&format!("UID:{}\n", contact.uid()));
|
||||
vcard.push_str("END:VCARD\n");
|
||||
|
||||
vcard
|
||||
@@ -176,16 +176,13 @@ impl ContactStorageAdapter {
|
||||
#[async_trait]
|
||||
impl AddressBookUseCase for ContactStorageAdapter {
|
||||
async fn create_address_book(&self, dto: CreateAddressBookDto) -> Result<AddressBookDto, DomainError> {
|
||||
let address_book = AddressBook {
|
||||
id: Uuid::new_v4(),
|
||||
name: dto.name,
|
||||
owner_id: dto.owner_id,
|
||||
description: dto.description,
|
||||
color: dto.color,
|
||||
is_public: dto.is_public.unwrap_or(false),
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let address_book = AddressBook::new(
|
||||
dto.name,
|
||||
dto.owner_id,
|
||||
dto.description,
|
||||
dto.color,
|
||||
dto.is_public.unwrap_or(false),
|
||||
);
|
||||
|
||||
let created = self.address_book_repository.create_address_book(address_book).await?;
|
||||
Ok(AddressBookDto::from(created))
|
||||
@@ -198,18 +195,18 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
||||
let mut address_book = self.check_write_access(&uuid, &update.user_id).await?;
|
||||
|
||||
if let Some(name) = update.name {
|
||||
address_book.name = name;
|
||||
address_book.set_name(name);
|
||||
}
|
||||
if let Some(description) = update.description {
|
||||
address_book.description = Some(description);
|
||||
address_book.set_description(Some(description));
|
||||
}
|
||||
if let Some(color) = update.color {
|
||||
address_book.color = Some(color);
|
||||
address_book.set_color(Some(color));
|
||||
}
|
||||
if let Some(is_public) = update.is_public {
|
||||
address_book.is_public = is_public;
|
||||
address_book.set_is_public(is_public);
|
||||
}
|
||||
address_book.updated_at = chrono::Utc::now();
|
||||
address_book.set_updated_at(chrono::Utc::now());
|
||||
|
||||
let updated = self.address_book_repository.update_address_book(address_book).await?;
|
||||
Ok(AddressBookDto::from(updated))
|
||||
@@ -224,7 +221,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
if address_book.owner_id != user_id {
|
||||
if address_book.owner_id() != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can delete address book"));
|
||||
}
|
||||
|
||||
@@ -261,7 +258,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
if address_book.owner_id != user_id {
|
||||
if address_book.owner_id() != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can share"));
|
||||
}
|
||||
|
||||
@@ -277,7 +274,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
if address_book.owner_id != user_id {
|
||||
if address_book.owner_id() != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can unshare"));
|
||||
}
|
||||
|
||||
@@ -293,7 +290,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
if address_book.owner_id != user_id {
|
||||
if address_book.owner_id() != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can view shares"));
|
||||
}
|
||||
|
||||
@@ -309,34 +306,35 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
// Check write access
|
||||
self.check_write_access(&address_book_id, &dto.user_id).await?;
|
||||
|
||||
let contact = Contact {
|
||||
id: Uuid::new_v4(),
|
||||
let now = chrono::Utc::now();
|
||||
let mut contact = Contact::from_raw(
|
||||
Uuid::new_v4(),
|
||||
address_book_id,
|
||||
uid: format!("{}@oxicloud", Uuid::new_v4()),
|
||||
full_name: dto.full_name,
|
||||
first_name: dto.first_name,
|
||||
last_name: dto.last_name,
|
||||
nickname: dto.nickname,
|
||||
email: dto.email.into_iter().map(Self::dto_to_email).collect(),
|
||||
phone: dto.phone.into_iter().map(Self::dto_to_phone).collect(),
|
||||
address: dto.address.into_iter().map(Self::dto_to_address).collect(),
|
||||
organization: dto.organization,
|
||||
title: dto.title,
|
||||
notes: dto.notes,
|
||||
photo_url: dto.photo_url,
|
||||
birthday: dto.birthday,
|
||||
anniversary: dto.anniversary,
|
||||
vcard: String::new(),
|
||||
etag: Uuid::new_v4().to_string(),
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
format!("{}@oxicloud", Uuid::new_v4()),
|
||||
dto.full_name,
|
||||
dto.first_name,
|
||||
dto.last_name,
|
||||
dto.nickname,
|
||||
dto.email.into_iter().map(Self::dto_to_email).collect(),
|
||||
dto.phone.into_iter().map(Self::dto_to_phone).collect(),
|
||||
dto.address.into_iter().map(Self::dto_to_address).collect(),
|
||||
dto.organization,
|
||||
dto.title,
|
||||
dto.notes,
|
||||
dto.photo_url,
|
||||
dto.birthday,
|
||||
dto.anniversary,
|
||||
String::new(),
|
||||
Uuid::new_v4().to_string(),
|
||||
now,
|
||||
now,
|
||||
);
|
||||
|
||||
// Generate vCard
|
||||
let mut contact_with_vcard = contact;
|
||||
contact_with_vcard.vcard = Self::generate_vcard(&contact_with_vcard);
|
||||
let vcard = Self::generate_vcard(&contact);
|
||||
contact.set_vcard(vcard);
|
||||
|
||||
let created = self.contact_repository.create_contact(contact_with_vcard).await?;
|
||||
let created = self.contact_repository.create_contact(contact).await?;
|
||||
Ok(ContactDto::from(created))
|
||||
}
|
||||
|
||||
@@ -347,28 +345,29 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
self.check_write_access(&address_book_id, &dto.user_id).await?;
|
||||
|
||||
// Parse vCard - for now, create a basic contact with the raw vCard
|
||||
let contact = Contact {
|
||||
id: Uuid::new_v4(),
|
||||
let now = chrono::Utc::now();
|
||||
let contact = Contact::from_raw(
|
||||
Uuid::new_v4(),
|
||||
address_book_id,
|
||||
uid: format!("{}@oxicloud", Uuid::new_v4()),
|
||||
full_name: Some("Imported Contact".to_string()),
|
||||
first_name: None,
|
||||
last_name: None,
|
||||
nickname: None,
|
||||
email: Vec::new(),
|
||||
phone: Vec::new(),
|
||||
address: Vec::new(),
|
||||
organization: None,
|
||||
title: None,
|
||||
notes: None,
|
||||
photo_url: None,
|
||||
birthday: None,
|
||||
anniversary: None,
|
||||
vcard: dto.vcard,
|
||||
etag: Uuid::new_v4().to_string(),
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
format!("{}@oxicloud", Uuid::new_v4()),
|
||||
Some("Imported Contact".to_string()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
dto.vcard,
|
||||
Uuid::new_v4().to_string(),
|
||||
now,
|
||||
now,
|
||||
);
|
||||
|
||||
let created = self.contact_repository.create_contact(contact).await?;
|
||||
Ok(ContactDto::from(created))
|
||||
@@ -383,51 +382,52 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
||||
|
||||
// Check write access to the address book
|
||||
self.check_write_access(&contact.address_book_id, &update.user_id).await?;
|
||||
self.check_write_access(contact.address_book_id(), &update.user_id).await?;
|
||||
|
||||
if let Some(full_name) = update.full_name {
|
||||
contact.full_name = Some(full_name);
|
||||
contact.set_full_name(Some(full_name));
|
||||
}
|
||||
if let Some(first_name) = update.first_name {
|
||||
contact.first_name = Some(first_name);
|
||||
contact.set_first_name(Some(first_name));
|
||||
}
|
||||
if let Some(last_name) = update.last_name {
|
||||
contact.last_name = Some(last_name);
|
||||
contact.set_last_name(Some(last_name));
|
||||
}
|
||||
if let Some(nickname) = update.nickname {
|
||||
contact.nickname = Some(nickname);
|
||||
contact.set_nickname(Some(nickname));
|
||||
}
|
||||
if let Some(emails) = update.email {
|
||||
contact.email = emails.into_iter().map(Self::dto_to_email).collect();
|
||||
contact.set_email(emails.into_iter().map(Self::dto_to_email).collect());
|
||||
}
|
||||
if let Some(phones) = update.phone {
|
||||
contact.phone = phones.into_iter().map(Self::dto_to_phone).collect();
|
||||
contact.set_phone(phones.into_iter().map(Self::dto_to_phone).collect());
|
||||
}
|
||||
if let Some(addresses) = update.address {
|
||||
contact.address = addresses.into_iter().map(Self::dto_to_address).collect();
|
||||
contact.set_address(addresses.into_iter().map(Self::dto_to_address).collect());
|
||||
}
|
||||
if let Some(organization) = update.organization {
|
||||
contact.organization = Some(organization);
|
||||
contact.set_organization(Some(organization));
|
||||
}
|
||||
if let Some(title) = update.title {
|
||||
contact.title = Some(title);
|
||||
contact.set_title(Some(title));
|
||||
}
|
||||
if let Some(notes) = update.notes {
|
||||
contact.notes = Some(notes);
|
||||
contact.set_notes(Some(notes));
|
||||
}
|
||||
if let Some(photo_url) = update.photo_url {
|
||||
contact.photo_url = Some(photo_url);
|
||||
contact.set_photo_url(Some(photo_url));
|
||||
}
|
||||
if let Some(birthday) = update.birthday {
|
||||
contact.birthday = Some(birthday);
|
||||
contact.set_birthday(Some(birthday));
|
||||
}
|
||||
if let Some(anniversary) = update.anniversary {
|
||||
contact.anniversary = Some(anniversary);
|
||||
contact.set_anniversary(Some(anniversary));
|
||||
}
|
||||
|
||||
contact.updated_at = chrono::Utc::now();
|
||||
contact.etag = Uuid::new_v4().to_string();
|
||||
contact.vcard = Self::generate_vcard(&contact);
|
||||
contact.set_updated_at(chrono::Utc::now());
|
||||
contact.set_etag(Uuid::new_v4().to_string());
|
||||
let vcard = Self::generate_vcard(&contact);
|
||||
contact.set_vcard(vcard);
|
||||
|
||||
let updated = self.contact_repository.update_contact(contact).await?;
|
||||
Ok(ContactDto::from(updated))
|
||||
@@ -442,7 +442,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(&contact.address_book_id, user_id).await?;
|
||||
self.check_write_access(contact.address_book_id(), user_id).await?;
|
||||
|
||||
self.contact_repository.delete_contact(&uuid).await
|
||||
}
|
||||
@@ -456,7 +456,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&contact.address_book_id, user_id).await?;
|
||||
self.check_address_book_access(contact.address_book_id(), user_id).await?;
|
||||
|
||||
Ok(ContactDto::from(contact))
|
||||
}
|
||||
@@ -487,13 +487,10 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
// Check write access
|
||||
self.check_write_access(&address_book_id, &dto.user_id).await?;
|
||||
|
||||
let group = ContactGroup {
|
||||
id: Uuid::new_v4(),
|
||||
let group = ContactGroup::new(
|
||||
address_book_id,
|
||||
name: dto.name,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
dto.name,
|
||||
);
|
||||
|
||||
let created = self.group_repository.create_group(group).await?;
|
||||
Ok(ContactGroupDto::from(created))
|
||||
@@ -508,10 +505,10 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(&group.address_book_id, &update.user_id).await?;
|
||||
self.check_write_access(group.address_book_id(), &update.user_id).await?;
|
||||
|
||||
group.name = update.name;
|
||||
group.updated_at = chrono::Utc::now();
|
||||
group.set_name(update.name);
|
||||
group.set_updated_at(chrono::Utc::now());
|
||||
|
||||
let updated = self.group_repository.update_group(group).await?;
|
||||
Ok(ContactGroupDto::from(updated))
|
||||
@@ -526,7 +523,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(&group.address_book_id, user_id).await?;
|
||||
self.check_write_access(group.address_book_id(), user_id).await?;
|
||||
|
||||
self.group_repository.delete_group(&uuid).await
|
||||
}
|
||||
@@ -540,7 +537,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&group.address_book_id, user_id).await?;
|
||||
self.check_address_book_access(group.address_book_id(), user_id).await?;
|
||||
|
||||
Ok(ContactGroupDto::from(group))
|
||||
}
|
||||
@@ -565,7 +562,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(&group.address_book_id, user_id).await?;
|
||||
self.check_write_access(group.address_book_id(), user_id).await?;
|
||||
|
||||
self.group_repository.add_contact_to_group(&group_id, &contact_id).await
|
||||
}
|
||||
@@ -580,7 +577,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(&group.address_book_id, user_id).await?;
|
||||
self.check_write_access(group.address_book_id(), user_id).await?;
|
||||
|
||||
self.group_repository.remove_contact_from_group(&group_id, &contact_id).await
|
||||
}
|
||||
@@ -594,7 +591,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&group.address_book_id, user_id).await?;
|
||||
self.check_address_book_access(group.address_book_id(), user_id).await?;
|
||||
|
||||
let contacts = self.group_repository.get_contacts_in_group(&uuid).await?;
|
||||
Ok(contacts.into_iter().map(ContactDto::from).collect())
|
||||
@@ -609,7 +606,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&contact.address_book_id, user_id).await?;
|
||||
self.check_address_book_access(contact.address_book_id(), user_id).await?;
|
||||
|
||||
let groups = self.group_repository.get_groups_for_contact(&uuid).await?;
|
||||
Ok(groups.into_iter().map(ContactGroupDto::from).collect())
|
||||
@@ -624,9 +621,9 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&contact.address_book_id, user_id).await?;
|
||||
self.check_address_book_access(contact.address_book_id(), user_id).await?;
|
||||
|
||||
Ok(contact.vcard)
|
||||
Ok(contact.vcard().to_string())
|
||||
}
|
||||
|
||||
async fn get_contacts_as_vcards(&self, address_book_id: &str, user_id: &str) -> Result<Vec<(String, String)>, DomainError> {
|
||||
@@ -639,7 +636,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
|
||||
Ok(contacts
|
||||
.into_iter()
|
||||
.map(|c| (c.id.to_string(), c.vcard))
|
||||
.map(|c| (c.id().to_string(), c.vcard().to_string()))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::application::ports::auth_ports::TokenServicePort;
|
||||
use crate::application::services::auth_application_service::AuthApplicationService;
|
||||
use crate::application::services::folder_service::FolderService;
|
||||
use crate::infrastructure::repositories::{UserPgRepository, SessionPgRepository};
|
||||
use crate::infrastructure::services::password_hasher::Argon2PasswordHasher;
|
||||
use crate::infrastructure::services::jwt_service::JwtTokenService;
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::common::di::AuthServices;
|
||||
|
||||
pub async fn create_auth_services(
|
||||
config: &AppConfig,
|
||||
pool: Arc<PgPool>,
|
||||
folder_service: Option<Arc<FolderService>>
|
||||
) -> Result<AuthServices> {
|
||||
// Crear servicio de tokens JWT (implementación de TokenServicePort)
|
||||
let token_service: Arc<dyn TokenServicePort> = Arc::new(JwtTokenService::new(
|
||||
config.auth.jwt_secret.clone(),
|
||||
config.auth.access_token_expiry_secs,
|
||||
config.auth.refresh_token_expiry_secs,
|
||||
));
|
||||
|
||||
// Crear servicio de hashing de contraseñas
|
||||
let password_hasher = Arc::new(Argon2PasswordHasher::new());
|
||||
|
||||
// Crear repositorios PostgreSQL
|
||||
let user_repository = Arc::new(UserPgRepository::new(pool.clone()));
|
||||
let session_repository = Arc::new(SessionPgRepository::new(pool.clone()));
|
||||
|
||||
// Crear servicio de aplicación de autenticación
|
||||
let mut auth_app_service = AuthApplicationService::new(
|
||||
user_repository,
|
||||
session_repository,
|
||||
password_hasher,
|
||||
token_service.clone(),
|
||||
);
|
||||
|
||||
// Configurar servicio de carpetas si está disponible
|
||||
if let Some(folder_svc) = folder_service {
|
||||
auth_app_service = auth_app_service.with_folder_service(folder_svc);
|
||||
}
|
||||
|
||||
// Empaquetar servicio en Arc
|
||||
let auth_application_service = Arc::new(auth_app_service);
|
||||
|
||||
Ok(AuthServices {
|
||||
token_service,
|
||||
auth_application_service,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use sqlx::{postgres::PgPoolOptions, PgPool, Row};
|
||||
use anyhow::Result;
|
||||
use std::time::Duration;
|
||||
use crate::common::config::AppConfig;
|
||||
|
||||
pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
||||
tracing::info!("Inicializando conexión a PostgreSQL con URL: {}",
|
||||
config.database.connection_string.replace("postgres://", "postgres://[user]:[pass]@"));
|
||||
|
||||
// Add a more robust connection attempt with retries
|
||||
let mut attempt = 0;
|
||||
const MAX_ATTEMPTS: usize = 3;
|
||||
|
||||
while attempt < MAX_ATTEMPTS {
|
||||
attempt += 1;
|
||||
tracing::info!("Intento de conexión a PostgreSQL #{}", attempt);
|
||||
|
||||
// Crear el pool de conexiones con las opciones de configuración
|
||||
match PgPoolOptions::new()
|
||||
.max_connections(config.database.max_connections)
|
||||
.min_connections(config.database.min_connections)
|
||||
.acquire_timeout(Duration::from_secs(config.database.connect_timeout_secs))
|
||||
.idle_timeout(Duration::from_secs(config.database.idle_timeout_secs))
|
||||
.max_lifetime(Duration::from_secs(config.database.max_lifetime_secs))
|
||||
.connect(&config.database.connection_string)
|
||||
.await {
|
||||
Ok(pool) => {
|
||||
// Verificar la conexión
|
||||
match sqlx::query("SELECT 1").execute(&pool).await {
|
||||
Ok(_) => {
|
||||
tracing::info!("Conexión a PostgreSQL establecida correctamente");
|
||||
|
||||
// Verify if migrations have been applied
|
||||
let migration_check = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')")
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
|
||||
match migration_check {
|
||||
Ok(row) => {
|
||||
let tables_exist: bool = row.get(0);
|
||||
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");
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
tracing::warn!("No se pudo verificar el estado de las migraciones. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(pool);
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Error al verificar conexión: {}", e);
|
||||
tracing::warn!("La base de datos parece no estar configurada. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
|
||||
if attempt >= MAX_ATTEMPTS {
|
||||
return Err(anyhow::anyhow!("Error al verificar la conexión a PostgreSQL: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Error al conectar a PostgreSQL: {}", e);
|
||||
if attempt >= MAX_ATTEMPTS {
|
||||
return Err(anyhow::anyhow!("Error en la conexión a PostgreSQL: {}", e));
|
||||
}
|
||||
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))
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod adapters;
|
||||
pub mod auth_factory;
|
||||
pub mod db;
|
||||
pub mod repositories;
|
||||
pub mod services;
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
|
||||
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
|
||||
/// Composite que envuelve `Arc<dyn FileReadPort>` + `Arc<dyn FileWritePort>`
|
||||
/// y delega cada método al port correspondiente.
|
||||
///
|
||||
/// Gracias al blanket impl `impl<T: FileReadPort + FileWritePort> FileStoragePort for T {}`
|
||||
/// este tipo obtiene `FileStoragePort` automáticamente.
|
||||
pub struct CompositeFileRepository {
|
||||
read: Arc<dyn FileReadPort>,
|
||||
write: Arc<dyn FileWritePort>,
|
||||
}
|
||||
|
||||
impl CompositeFileRepository {
|
||||
pub fn new(read: Arc<dyn FileReadPort>, write: Arc<dyn FileWritePort>) -> Self {
|
||||
Self { read, write }
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// FileReadPort — delegate to self.read
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
#[async_trait]
|
||||
impl FileReadPort for CompositeFileRepository {
|
||||
async fn get_file(&self, id: &str) -> Result<File, DomainError> {
|
||||
self.read.get_file(id).await
|
||||
}
|
||||
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError> {
|
||||
self.read.list_files(folder_id).await
|
||||
}
|
||||
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError> {
|
||||
self.read.get_file_content(id).await
|
||||
}
|
||||
|
||||
async fn get_file_stream(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
||||
self.read.get_file_stream(id).await
|
||||
}
|
||||
|
||||
async fn get_file_range_stream(
|
||||
&self,
|
||||
id: &str,
|
||||
start: u64,
|
||||
end: Option<u64>,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
||||
self.read.get_file_range_stream(id, start, end).await
|
||||
}
|
||||
|
||||
async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError> {
|
||||
self.read.get_file_mmap(id).await
|
||||
}
|
||||
|
||||
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError> {
|
||||
self.read.get_file_path(id).await
|
||||
}
|
||||
|
||||
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError> {
|
||||
self.read.get_parent_folder_id(path).await
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// FileWritePort — delegate to self.write
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
#[async_trait]
|
||||
impl FileWritePort for CompositeFileRepository {
|
||||
async fn save_file(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> Result<File, DomainError> {
|
||||
self.write.save_file(name, folder_id, content_type, content).await
|
||||
}
|
||||
|
||||
async fn save_file_from_stream(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
||||
) -> Result<File, DomainError> {
|
||||
self.write.save_file_from_stream(name, folder_id, content_type, stream).await
|
||||
}
|
||||
|
||||
async fn move_file(
|
||||
&self,
|
||||
file_id: &str,
|
||||
target_folder_id: Option<String>,
|
||||
) -> Result<File, DomainError> {
|
||||
self.write.move_file(file_id, target_folder_id).await
|
||||
}
|
||||
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
|
||||
self.write.delete_file(id).await
|
||||
}
|
||||
|
||||
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError> {
|
||||
self.write.update_file_content(file_id, content).await
|
||||
}
|
||||
|
||||
async fn register_file_deferred(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
size: u64,
|
||||
) -> Result<(File, PathBuf), DomainError> {
|
||||
self.write.register_file_deferred(name, folder_id, content_type, size).await
|
||||
}
|
||||
|
||||
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError> {
|
||||
self.write.move_to_trash(file_id).await
|
||||
}
|
||||
|
||||
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError> {
|
||||
self.write.restore_from_trash(file_id, original_path).await
|
||||
}
|
||||
|
||||
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError> {
|
||||
self.write.delete_file_permanently(file_id).await
|
||||
}
|
||||
}
|
||||
@@ -1,186 +1,367 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::{fs, time};
|
||||
use tokio::fs::File as TokioFile;
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
use futures::{Stream, StreamExt};
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use tokio::task;
|
||||
use mime_guess::from_path;
|
||||
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::application::ports::storage_ports::FileReadPort;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::repositories::file_repository::FileRepositoryResult;
|
||||
use crate::infrastructure::repositories::file_metadata_manager::{FileMetadataManager, MetadataError};
|
||||
use crate::infrastructure::repositories::file_path_resolver::FilePathResolver;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::infrastructure::repositories::repository_errors::{FileRepositoryResult, FileRepositoryError};
|
||||
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||
use crate::application::ports::cache_ports::MetadataCachePort;
|
||||
use crate::application::services::storage_mediator::StorageMediator;
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::config::AppConfig;
|
||||
|
||||
/// Implementación de repositorio para operaciones de lectura de archivos
|
||||
/// Implementación de repositorio para operaciones de **lectura** de archivos.
|
||||
///
|
||||
/// Implementa `FileReadPort`:
|
||||
/// get_file, list_files, get_file_content, get_file_stream,
|
||||
/// get_file_range_stream, get_file_mmap, get_file_path, get_parent_folder_id.
|
||||
pub struct FileFsReadRepository {
|
||||
metadata_manager: Arc<FileMetadataManager>,
|
||||
path_resolver: Arc<FilePathResolver>,
|
||||
root_path: PathBuf,
|
||||
storage_mediator: Arc<dyn StorageMediator>,
|
||||
id_mapping_service: Arc<dyn crate::application::ports::outbound::IdMappingPort>,
|
||||
path_service: Arc<PathService>,
|
||||
metadata_cache: Arc<dyn MetadataCachePort>,
|
||||
config: AppConfig,
|
||||
parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
}
|
||||
|
||||
impl FileFsReadRepository {
|
||||
/// Crea un nuevo repositorio de lectura de archivos
|
||||
/// Constructor completo con todas las dependencias de infraestructura.
|
||||
pub fn new(
|
||||
_root_path: PathBuf,
|
||||
metadata_manager: Arc<FileMetadataManager>,
|
||||
path_resolver: Arc<FilePathResolver>,
|
||||
_config: AppConfig,
|
||||
_parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
root_path: PathBuf,
|
||||
storage_mediator: Arc<dyn StorageMediator>,
|
||||
id_mapping_service: Arc<dyn crate::application::ports::outbound::IdMappingPort>,
|
||||
path_service: Arc<PathService>,
|
||||
metadata_cache: Arc<dyn MetadataCachePort>,
|
||||
config: AppConfig,
|
||||
parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
metadata_manager,
|
||||
path_resolver,
|
||||
root_path,
|
||||
storage_mediator,
|
||||
id_mapping_service,
|
||||
path_service,
|
||||
metadata_cache,
|
||||
config,
|
||||
parallel_processor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un stub para pruebas
|
||||
|
||||
/// Stub para pruebas (no realiza I/O real).
|
||||
pub fn default_stub() -> Self {
|
||||
Self {
|
||||
metadata_manager: Arc::new(FileMetadataManager::default()),
|
||||
path_resolver: Arc::new(FilePathResolver::default_stub()),
|
||||
root_path: PathBuf::from("./storage"),
|
||||
storage_mediator: Arc::new(
|
||||
crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub(),
|
||||
),
|
||||
id_mapping_service: Arc::new(crate::common::stubs::StubIdMappingPort),
|
||||
path_service: Arc::new(PathService::new(PathBuf::from("./storage"))),
|
||||
metadata_cache: Arc::new(
|
||||
crate::infrastructure::services::file_metadata_cache::FileMetadataCache::default()
|
||||
) as Arc<dyn MetadataCachePort>,
|
||||
config: AppConfig::default(),
|
||||
parallel_processor: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea una entidad de archivo a partir de metadatos
|
||||
async fn create_file_entity(
|
||||
&self,
|
||||
id: String,
|
||||
name: String,
|
||||
storage_path: StoragePath,
|
||||
size: u64,
|
||||
mime_type: String,
|
||||
folder_id: Option<String>,
|
||||
created_at: Option<u64>,
|
||||
modified_at: Option<u64>,
|
||||
) -> FileRepositoryResult<File> {
|
||||
// If timestamps are provided, use them; otherwise, let File::new create default timestamps
|
||||
if let (Some(created), Some(modified)) = (created_at, modified_at) {
|
||||
File::with_timestamps(
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
size,
|
||||
mime_type,
|
||||
folder_id,
|
||||
created,
|
||||
modified,
|
||||
)
|
||||
.map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string()))
|
||||
} else {
|
||||
File::new(
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
size,
|
||||
mime_type,
|
||||
folder_id,
|
||||
)
|
||||
.map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string()))
|
||||
}
|
||||
|
||||
// ─── helpers internos ────────────────────────────────────
|
||||
|
||||
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
self.path_service.resolve_path(storage_path)
|
||||
}
|
||||
|
||||
/// Obtiene un archivo por su ID
|
||||
|
||||
async fn get_file_metadata_raw(&self, abs_path: &PathBuf) -> FileRepositoryResult<(u64, u64, u64)> {
|
||||
// Cache first
|
||||
if let Some(cached) = self.metadata_cache.get_metadata(abs_path).await {
|
||||
if let (Some(s), Some(c), Some(m)) = (cached.size, cached.created_at, cached.modified_at) {
|
||||
return Ok((s, c, m));
|
||||
}
|
||||
}
|
||||
let metadata = time::timeout(self.config.timeouts.file_timeout(), fs::metadata(abs_path))
|
||||
.await
|
||||
.map_err(|_| FileRepositoryError::StorageError(format!("Timeout metadata: {}", abs_path.display())))?
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
let size = metadata.len();
|
||||
let created_at = metadata.created()
|
||||
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
.unwrap_or(0);
|
||||
let modified_at = metadata.modified()
|
||||
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
.unwrap_or(0);
|
||||
let _ = self.metadata_cache.refresh_metadata(abs_path).await;
|
||||
Ok((size, created_at, modified_at))
|
||||
}
|
||||
|
||||
async fn get_file_by_id(&self, id: &str) -> FileRepositoryResult<File> {
|
||||
// Obtener la ruta del archivo usando el resolver de rutas
|
||||
let storage_path = self.path_resolver.get_path_by_id(id).await?;
|
||||
|
||||
// Verificar que el archivo existe físicamente
|
||||
let abs_path = self.path_resolver.resolve_storage_path(&storage_path);
|
||||
if !self.metadata_manager.file_exists(&abs_path).await
|
||||
.map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string()))? {
|
||||
return Err(crate::domain::repositories::file_repository::FileRepositoryError::NotFound(
|
||||
format!("File {} not found at {}", id, storage_path.to_string())
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(id).await
|
||||
.map_err(|e| FileRepositoryError::Other(e.to_string()))?;
|
||||
let abs_path = self.resolve_storage_path(&storage_path);
|
||||
|
||||
if !abs_path.exists() || !abs_path.is_file() {
|
||||
return Err(FileRepositoryError::NotFound(
|
||||
format!("File {} not found at {}", id, storage_path.to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
// Obtener metadatos del archivo
|
||||
let (size, created_at, modified_at) = self.metadata_manager.get_file_metadata(&abs_path).await
|
||||
.map_err(|e| match e {
|
||||
MetadataError::IoError(io_err) => crate::domain::repositories::file_repository::FileRepositoryError::IoError(io_err),
|
||||
MetadataError::Timeout(msg) => crate::domain::repositories::file_repository::FileRepositoryError::Timeout(msg),
|
||||
MetadataError::Unavailable(msg) => crate::domain::repositories::file_repository::FileRepositoryError::NotFound(msg),
|
||||
})?;
|
||||
|
||||
// Obtener nombre del archivo de la ruta
|
||||
let name = match storage_path.file_name() {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
return Err(crate::domain::repositories::file_repository::FileRepositoryError::InvalidPath(
|
||||
storage_path.to_string()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Determinar ID de carpeta padre
|
||||
let parent = storage_path.parent();
|
||||
let folder_id: Option<String> = if parent.is_none() || parent.as_ref().unwrap().is_empty() {
|
||||
None // Root folder
|
||||
} else {
|
||||
None // En implementación real, buscar ID de la carpeta padre
|
||||
};
|
||||
|
||||
// Determinar tipo MIME
|
||||
let mime_type = mime_guess::from_path(&abs_path)
|
||||
.first_or_octet_stream()
|
||||
.to_string();
|
||||
|
||||
// Crear entidad de archivo
|
||||
let file = self.create_file_entity(
|
||||
id.to_string(),
|
||||
name,
|
||||
storage_path,
|
||||
size,
|
||||
mime_type,
|
||||
folder_id,
|
||||
Some(created_at),
|
||||
Some(modified_at),
|
||||
).await?;
|
||||
|
||||
Ok(file)
|
||||
|
||||
let (size, created_at, modified_at) = self.get_file_metadata_raw(&abs_path).await?;
|
||||
let name = storage_path
|
||||
.file_name()
|
||||
.ok_or_else(|| FileRepositoryError::InvalidPath(storage_path.to_string()))?;
|
||||
let mime_type = from_path(&abs_path).first_or_octet_stream().to_string();
|
||||
|
||||
File::with_timestamps(
|
||||
id.to_string(), name, storage_path, size, mime_type, None,
|
||||
created_at, modified_at,
|
||||
)
|
||||
.map_err(|e| FileRepositoryError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for FileFsReadRepository {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
root_path: self.root_path.clone(),
|
||||
storage_mediator: self.storage_mediator.clone(),
|
||||
id_mapping_service: self.id_mapping_service.clone(),
|
||||
path_service: self.path_service.clone(),
|
||||
metadata_cache: self.metadata_cache.clone(),
|
||||
config: self.config.clone(),
|
||||
parallel_processor: self.parallel_processor.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FileReadPort for FileFsReadRepository {
|
||||
async fn get_file(&self, id: &str) -> Result<File, DomainError> {
|
||||
self.get_file_by_id(id).await
|
||||
.map_err(|e| match e {
|
||||
crate::domain::repositories::file_repository::FileRepositoryError::NotFound(msg) => DomainError::not_found("File", msg),
|
||||
crate::domain::repositories::file_repository::FileRepositoryError::IoError(io_err) => DomainError::internal_error("File", io_err.to_string()),
|
||||
crate::domain::repositories::file_repository::FileRepositoryError::Timeout(msg) => DomainError::internal_error("File", msg),
|
||||
_ => DomainError::internal_error("File", e.to_string()),
|
||||
})
|
||||
self.get_file_by_id(id).await.map_err(|e| match e {
|
||||
FileRepositoryError::NotFound(msg) => DomainError::not_found("File", msg),
|
||||
FileRepositoryError::StorageError(msg) => DomainError::internal_error("File", msg),
|
||||
other => DomainError::internal_error("File", other.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn list_files(&self, _folder_id: Option<&str>) -> Result<Vec<File>, DomainError> {
|
||||
// Implementación real debe obtener la lista de archivos en una carpeta
|
||||
// Por ahora, devolvemos lista vacía
|
||||
Ok(Vec::new())
|
||||
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError> {
|
||||
let folder_storage_path = match folder_id {
|
||||
Some(id) => {
|
||||
match self.storage_mediator.get_folder_path(id).await {
|
||||
Ok(path) => {
|
||||
let lossy = path.to_string_lossy().to_string();
|
||||
let folder_name = path.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.unwrap_or(&lossy);
|
||||
StoragePath::from_string(folder_name)
|
||||
}
|
||||
Err(_) => return Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
None => StoragePath::root(),
|
||||
};
|
||||
|
||||
let abs_folder_path = self.path_service.resolve_path(&folder_storage_path);
|
||||
if !abs_folder_path.exists() || !abs_folder_path.is_dir() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut files_result = Vec::new();
|
||||
let mut entries = fs::read_dir(&abs_folder_path).await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
while let Some(entry) = entries.next_entry().await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?
|
||||
{
|
||||
let path = entry.path();
|
||||
if !path.is_file() { continue; }
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
if file_name.starts_with('.') || file_name == "folder_ids.json" || file_name == "file_ids.json" {
|
||||
continue;
|
||||
}
|
||||
let metadata = match fs::metadata(&path).await {
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let file_storage_path = folder_storage_path.join(&file_name);
|
||||
let id = match self.id_mapping_service.get_or_create_id(&file_storage_path).await {
|
||||
Ok(id) => id,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let size = metadata.len();
|
||||
let created_at = metadata.created()
|
||||
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
.unwrap_or(0);
|
||||
let modified_at = metadata.modified()
|
||||
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
.unwrap_or(0);
|
||||
let mime_type = from_path(&path).first_or_octet_stream().to_string();
|
||||
|
||||
match File::with_timestamps(id, file_name, file_storage_path, size, mime_type, folder_id.map(String::from), created_at, modified_at) {
|
||||
Ok(file) => files_result.push(file),
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
// Persist any new ID mappings
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
Ok(files_result)
|
||||
}
|
||||
|
||||
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError> {
|
||||
// Primero obtenemos el archivo para verificar existencia
|
||||
let file = self.get_file_by_id(id).await
|
||||
.map_err(|e| match e {
|
||||
crate::domain::repositories::file_repository::FileRepositoryError::NotFound(msg) => DomainError::not_found("File", msg),
|
||||
crate::domain::repositories::file_repository::FileRepositoryError::IoError(io_err) => DomainError::internal_error("File", io_err.to_string()),
|
||||
crate::domain::repositories::file_repository::FileRepositoryError::Timeout(msg) => DomainError::internal_error("File", msg),
|
||||
_ => DomainError::internal_error("File", e.to_string()),
|
||||
})?;
|
||||
|
||||
// Ruta absoluta del archivo
|
||||
let _abs_path = self.path_resolver.resolve_storage_path(file.storage_path());
|
||||
|
||||
// Implementación real debe leer el contenido del archivo
|
||||
// Por ahora, devolvemos un vector vacío
|
||||
Ok(Vec::new())
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let abs_path = self.resolve_storage_path(file.storage_path());
|
||||
|
||||
let metadata = time::timeout(self.config.timeouts.file_timeout(), fs::metadata(&abs_path))
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", format!("Timeout metadata: {}", abs_path.display())))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let file_size = metadata.len();
|
||||
|
||||
if !self.config.resources.can_load_in_memory(file_size) {
|
||||
return Err(DomainError::internal_error("File",
|
||||
format!("File too large for memory: {} MB", file_size / (1024 * 1024))));
|
||||
}
|
||||
|
||||
// Parallel read for very large files
|
||||
if self.config.resources.needs_parallel_processing(file_size, &self.config.concurrency) {
|
||||
let content = if let Some(processor) = &self.parallel_processor {
|
||||
processor.read_file_parallel(&abs_path).await
|
||||
} else {
|
||||
let processor = ParallelFileProcessor::new(self.config.clone());
|
||||
processor.read_file_parallel(&abs_path).await
|
||||
};
|
||||
return content.map_err(|e| DomainError::internal_error("File", e.to_string()));
|
||||
}
|
||||
|
||||
// spawn_blocking for large-ish files
|
||||
if self.config.resources.is_large_file(file_size) {
|
||||
let abs_clone = abs_path.clone();
|
||||
let chunk_size = self.config.resources.chunk_size_bytes;
|
||||
let content = task::spawn_blocking(move || -> std::io::Result<Vec<u8>> {
|
||||
use std::io::{Read, BufReader};
|
||||
let file = std::fs::File::open(&abs_clone)?;
|
||||
let mut reader = BufReader::with_capacity(chunk_size, file);
|
||||
let mut buf = Vec::with_capacity(file_size as usize);
|
||||
reader.read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
}).await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
return Ok(content);
|
||||
}
|
||||
|
||||
// Small files — async read
|
||||
time::timeout(self.config.timeouts.file_timeout(), fs::read(&abs_path))
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", format!("Timeout reading: {}", abs_path.display())))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))
|
||||
}
|
||||
|
||||
async fn get_file_stream(&self, _id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
||||
// Implementación real debe devolver un stream de bytes del archivo
|
||||
// Por ahora, lanzamos un error
|
||||
Err(DomainError::internal_error("File stream", "Stream functionality not yet implemented"))
|
||||
|
||||
async fn get_file_stream(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
||||
let file = self.get_file_by_id(id).await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let abs_path = self.resolve_storage_path(file.storage_path());
|
||||
|
||||
let metadata = time::timeout(self.config.timeouts.file_timeout(), fs::metadata(&abs_path))
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout getting metadata"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let file_size = metadata.len();
|
||||
let is_large = self.config.resources.is_large_file(file_size);
|
||||
|
||||
let fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::open(&abs_path))
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout opening file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
let chunk_size = if is_large { self.config.resources.chunk_size_bytes } else { 4096 };
|
||||
let codec = BytesCodec::new();
|
||||
let stream = FramedRead::with_capacity(fh, codec, chunk_size)
|
||||
.map(|r| r.map(|bm| bm.freeze()));
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
|
||||
async fn get_file_range_stream(
|
||||
&self,
|
||||
id: &str,
|
||||
start: u64,
|
||||
end: Option<u64>,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
||||
use tokio::io::AsyncSeekExt;
|
||||
|
||||
let file = self.get_file_by_id(id).await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let abs_path = self.resolve_storage_path(file.storage_path());
|
||||
|
||||
let metadata = time::timeout(self.config.timeouts.file_timeout(), fs::metadata(&abs_path))
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let file_size = metadata.len();
|
||||
if start >= file_size {
|
||||
return Err(DomainError::internal_error("File",
|
||||
format!("Range start {} beyond file size {}", start, file_size)));
|
||||
}
|
||||
let actual_end = end.map(|e| e.min(file_size - 1)).unwrap_or(file_size - 1);
|
||||
let range_length = actual_end - start + 1;
|
||||
|
||||
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::open(&abs_path))
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout opening file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.seek(std::io::SeekFrom::Start(start)).await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
let chunk_size = if range_length > 1024 * 1024 { self.config.resources.chunk_size_bytes } else { 8192 };
|
||||
use tokio::io::AsyncReadExt;
|
||||
let limited = fh.take(range_length);
|
||||
let codec = BytesCodec::new();
|
||||
let stream = FramedRead::with_capacity(limited, codec, chunk_size)
|
||||
.map(|r| r.map(|bm| bm.freeze()));
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
|
||||
async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError> {
|
||||
use memmap2::Mmap;
|
||||
let file = self.get_file_by_id(id).await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let abs_path = self.resolve_storage_path(file.storage_path());
|
||||
let path_clone = abs_path.clone();
|
||||
|
||||
task::spawn_blocking(move || -> Result<Bytes, DomainError> {
|
||||
let fh = std::fs::File::open(&path_clone)
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let mmap = unsafe { Mmap::map(&fh) }
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
Ok(Bytes::copy_from_slice(&mmap[..]))
|
||||
}).await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?
|
||||
}
|
||||
|
||||
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError> {
|
||||
self.id_mapping_service.get_path_by_id(id).await
|
||||
}
|
||||
|
||||
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError> {
|
||||
let storage_path = StoragePath::from_string(path);
|
||||
match storage_path.parent() {
|
||||
Some(parent) if !parent.is_empty() => {
|
||||
self.id_mapping_service.get_or_create_id(&parent).await
|
||||
}
|
||||
_ => Ok("root".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,368 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
use tracing::{debug, error, instrument};
|
||||
|
||||
use crate::domain::repositories::file_repository::FileRepositoryResult;
|
||||
use crate::infrastructure::repositories::file_fs_repository::FileFsRepository;
|
||||
|
||||
// This file contains the implementation of trash-related methods
|
||||
// for the FileFsRepository file repository
|
||||
|
||||
// Implementation of trash methods for the file repository
|
||||
impl FileFsRepository {
|
||||
// Gets the complete path to the trash directory
|
||||
fn get_trash_dir(&self) -> PathBuf {
|
||||
let trash_dir = self.get_root_path().join(".trash").join("files");
|
||||
debug!("Base trash directory: {}", trash_dir.display());
|
||||
trash_dir
|
||||
}
|
||||
|
||||
// Gets the trash directory path for a specific user (if provided)
|
||||
fn get_user_trash_dir(&self, user_id: Option<&str>) -> PathBuf {
|
||||
let base_trash_dir = self.get_trash_dir();
|
||||
|
||||
if let Some(uid) = user_id {
|
||||
let user_trash_dir = base_trash_dir.join(uid);
|
||||
debug!("User-specific trash directory: {}", user_trash_dir.display());
|
||||
user_trash_dir
|
||||
} else {
|
||||
// No user ID provided - this should not happen in production
|
||||
tracing::warn!("No user_id provided for trash directory, this indicates a bug");
|
||||
let default_dir = base_trash_dir.join("unknown-user");
|
||||
debug!("Fallback user trash directory: {}", default_dir.display());
|
||||
default_dir
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a unique path in the trash for the file
|
||||
async fn create_trash_file_path(&self, file_id: &str) -> FileRepositoryResult<PathBuf> {
|
||||
debug!("Creating trash file path for file ID: {}", file_id);
|
||||
|
||||
// Get the trash directory for the default user
|
||||
let user_trash_dir = self.get_user_trash_dir(None);
|
||||
|
||||
// Ensure the user's trash directory exists
|
||||
debug!("Ensuring user trash directory exists: {}", user_trash_dir.display());
|
||||
if !user_trash_dir.exists() {
|
||||
debug!("Creating user trash directory: {}", user_trash_dir.display());
|
||||
fs::create_dir_all(&user_trash_dir).await
|
||||
.map_err(|e| {
|
||||
error!("Failed to create user trash directory: {}", e);
|
||||
FileRepositoryError::IoError(e)
|
||||
})?;
|
||||
debug!("User trash directory created successfully");
|
||||
} else {
|
||||
debug!("User trash directory already exists");
|
||||
}
|
||||
|
||||
// Create a unique path for the file in the trash
|
||||
let trash_file_path = user_trash_dir.join(file_id);
|
||||
debug!("Trash file path: {}", trash_file_path.display());
|
||||
|
||||
Ok(trash_file_path)
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation of the public methods of the FileRepository trait related to trash
|
||||
// Note: The FileRepository trait implementation has been moved to file_fs_repository.rs
|
||||
// to avoid duplicate implementations
|
||||
|
||||
// Implementation of internal methods for trash functionality
|
||||
impl FileFsRepository {
|
||||
/// Helper method that will be used for trash functionality
|
||||
pub(crate) async fn _trash_move_to_trash(&self, file_id: &str) -> FileRepositoryResult<()> {
|
||||
debug!("Moving file to trash: {}", file_id);
|
||||
|
||||
// Get the physical path of the file
|
||||
// We create an independent method to access the ID mapping service
|
||||
debug!("Getting file path with ID: {}", file_id);
|
||||
let file_path = match self.id_mapping_service().get_file_path(file_id).await {
|
||||
Ok(path) => {
|
||||
debug!("File path obtained: {}", path.display());
|
||||
path
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error getting file path {}: {:?}", file_id, e);
|
||||
return Err(FileRepositoryError::IdMappingError(format!("Failed to get file path: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
// Verify that the file exists
|
||||
debug!("Verifying that the file exists: {}", file_path.display());
|
||||
if !self.file_exists(&file_path).await? {
|
||||
error!("File not found at the specified path: {}", file_path.display());
|
||||
return Err(FileRepositoryError::NotFound(format!("File not found: {}", file_id)));
|
||||
}
|
||||
debug!("File found, continuing with the operation");
|
||||
|
||||
// Create directory in trash if it doesn't exist
|
||||
debug!("Creating path for file in trash");
|
||||
let trash_file_path = self.create_trash_file_path(file_id).await?;
|
||||
debug!("Path in trash: {}", trash_file_path.display());
|
||||
|
||||
// Physically move the file to trash (doesn't update mappings)
|
||||
debug!("Physically moving file to trash: {} -> {}", file_path.display(), trash_file_path.display());
|
||||
match fs::rename(&file_path, &trash_file_path).await {
|
||||
Ok(_) => {
|
||||
debug!("File successfully moved to trash: {} -> {}", file_path.display(), trash_file_path.display());
|
||||
|
||||
// Invalidate the cache for the original file
|
||||
debug!("Invalidating cache for: {}", file_path.display());
|
||||
self.metadata_cache().invalidate(&file_path).await;
|
||||
|
||||
// Update the mapping to the new path in trash
|
||||
debug!("Updating ID mapping to new path in trash");
|
||||
if let Err(e) = self.id_mapping_service().update_file_path(file_id, &trash_file_path).await {
|
||||
error!("Error updating file mapping in trash: {}", e);
|
||||
return Err(FileRepositoryError::MappingError(format!("Failed to update mapping: {}", e)));
|
||||
}
|
||||
debug!("Mapping successfully updated");
|
||||
|
||||
debug!("Move to trash operation completed successfully for file: {}", file_id);
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error moving file to trash: {} -> {}: {}",
|
||||
file_path.display(), trash_file_path.display(), e);
|
||||
Err(FileRepositoryError::IoError(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Restores a file from trash to its original location
|
||||
#[instrument(skip(self))]
|
||||
pub(crate) async fn _trash_restore_from_trash(&self, file_id: &str, original_path: &str) -> FileRepositoryResult<()> {
|
||||
debug!("Restoring file {} to {}", file_id, original_path);
|
||||
|
||||
// Try to get the current path from the ID mapping service
|
||||
let current_path_result = self.id_mapping_service().get_file_path(file_id).await;
|
||||
|
||||
match current_path_result {
|
||||
Ok(current_path) => {
|
||||
debug!("Current path in trash: {}", current_path.display());
|
||||
|
||||
// Check if the file exists in the trash
|
||||
let file_exists = match fs::metadata(¤t_path).await {
|
||||
Ok(_) => {
|
||||
debug!("File exists in trash");
|
||||
true
|
||||
},
|
||||
Err(e) => {
|
||||
debug!("File does not exist in trash: {} - {}", current_path.display(), e);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !file_exists {
|
||||
error!("The file does not physically exist in the trash: {}", current_path.display());
|
||||
return Err(FileRepositoryError::NotFound(format!("File not found in trash: {}", file_id)));
|
||||
}
|
||||
|
||||
// Parse the original path to a PathBuf
|
||||
let original_path_buf = PathBuf::from(original_path);
|
||||
debug!("Original path for restoration: {}", original_path_buf.display());
|
||||
|
||||
// Check if a file already exists at the destination
|
||||
let target_exists = fs::metadata(&original_path_buf).await.is_ok();
|
||||
if target_exists {
|
||||
debug!("A file already exists at the destination path, generating alternative path");
|
||||
|
||||
// Generate a unique path by adding a suffix
|
||||
// Extract filename and extension
|
||||
let file_name = original_path_buf.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "restored_file".to_string());
|
||||
|
||||
let parent_dir = original_path_buf.parent()
|
||||
.unwrap_or_else(|| std::path::Path::new(""));
|
||||
|
||||
let (stem, ext) = if let Some(dot_pos) = file_name.rfind('.') {
|
||||
(file_name[..dot_pos].to_string(), file_name[dot_pos..].to_string())
|
||||
} else {
|
||||
(file_name, "".to_string())
|
||||
};
|
||||
|
||||
// Create a new name with a timestamp
|
||||
let timestamp = chrono::Utc::now().timestamp();
|
||||
let new_name = format!("{}_{}{}", stem, timestamp, ext);
|
||||
|
||||
// Create the alternative path
|
||||
let alternative_path = parent_dir.join(new_name);
|
||||
debug!("Alternative path for restoration: {}", alternative_path.display());
|
||||
|
||||
// Ensure the parent directory exists
|
||||
if let Some(parent) = alternative_path.parent() {
|
||||
if !parent.exists() {
|
||||
debug!("Creating parent directory for restoration: {}", parent.display());
|
||||
match fs::create_dir_all(parent).await {
|
||||
Ok(_) => debug!("Parent directory created successfully"),
|
||||
Err(e) => {
|
||||
error!("Error creating parent directory: {} - {}", parent.display(), e);
|
||||
return Err(FileRepositoryError::IoError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move the file from trash to the alternative location
|
||||
debug!("Moving file from trash to alternative location: {} -> {}",
|
||||
current_path.display(), alternative_path.display());
|
||||
match fs::rename(¤t_path, &alternative_path).await {
|
||||
Ok(_) => {
|
||||
debug!("File successfully restored to alternative location");
|
||||
|
||||
// Invalidate cache entries
|
||||
debug!("Invalidating cache for file in trash");
|
||||
self.metadata_cache().invalidate(¤t_path).await;
|
||||
|
||||
// Update the ID mapping
|
||||
debug!("Updating ID mapping to new location");
|
||||
if let Err(e) = self.id_mapping_service().update_file_path(file_id, &alternative_path).await {
|
||||
error!("Error updating mapping of restored file: {}", e);
|
||||
return Err(FileRepositoryError::MappingError(
|
||||
format!("Failed to update mapping: {}", e)
|
||||
));
|
||||
}
|
||||
|
||||
debug!("Restoration to alternative location completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error restoring file to alternative location: {}", e);
|
||||
Err(FileRepositoryError::IoError(e))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ensure the parent directory exists
|
||||
if let Some(parent) = original_path_buf.parent() {
|
||||
if !parent.exists() {
|
||||
debug!("Creating parent directory for restoration: {}", parent.display());
|
||||
match fs::create_dir_all(parent).await {
|
||||
Ok(_) => debug!("Parent directory created successfully"),
|
||||
Err(e) => {
|
||||
error!("Error creating parent directory: {} - {}", parent.display(), e);
|
||||
return Err(FileRepositoryError::IoError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move the file from trash to its original location
|
||||
debug!("Moving file from trash to original location: {} -> {}",
|
||||
current_path.display(), original_path_buf.display());
|
||||
match fs::rename(¤t_path, &original_path_buf).await {
|
||||
Ok(_) => {
|
||||
debug!("File successfully restored to original location");
|
||||
|
||||
// Invalidate cache entries
|
||||
debug!("Invalidating cache for file in trash");
|
||||
self.metadata_cache().invalidate(¤t_path).await;
|
||||
|
||||
// Update the ID mapping
|
||||
debug!("Updating ID mapping to original location");
|
||||
if let Err(e) = self.id_mapping_service().update_file_path(file_id, &original_path_buf).await {
|
||||
error!("Error updating mapping of restored file: {}", e);
|
||||
return Err(FileRepositoryError::MappingError(
|
||||
format!("Failed to update mapping: {}", e)
|
||||
));
|
||||
}
|
||||
|
||||
debug!("Restoration to original location completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error restoring file to original location: {}", e);
|
||||
Err(FileRepositoryError::IoError(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error getting current path of file {}: {:?}", file_id, e);
|
||||
|
||||
// Check if the error is because the ID was not found
|
||||
if format!("{}", e).contains("not found") {
|
||||
debug!("ID not found in mapping, file no longer exists in trash");
|
||||
return Err(FileRepositoryError::NotFound(format!("File not found in trash: {}", file_id)));
|
||||
}
|
||||
|
||||
return Err(FileRepositoryError::IdMappingError(
|
||||
format!("Failed to get file path: {}", e)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Permanently deletes a file (used by trash)
|
||||
#[instrument(skip(self))]
|
||||
pub(crate) async fn _trash_delete_file_permanently(&self, file_id: &str) -> FileRepositoryResult<()> {
|
||||
debug!("Permanently deleting file: {}", file_id);
|
||||
|
||||
// Get the file path using the ID mapping service
|
||||
let file_path_result = self.id_mapping_service().get_file_path(file_id).await;
|
||||
|
||||
match file_path_result {
|
||||
Ok(file_path) => {
|
||||
debug!("Found path for file: {} -> {}", file_id, file_path.display());
|
||||
|
||||
// Check if the file physically exists before attempting to delete
|
||||
let file_exists = fs::metadata(&file_path).await.is_ok();
|
||||
|
||||
if file_exists {
|
||||
debug!("File exists physically, deleting: {}", file_path.display());
|
||||
|
||||
// Delete the file physically
|
||||
if let Err(e) = fs::remove_file(&file_path).await {
|
||||
error!("Error permanently deleting file: {} - {}", file_path.display(), e);
|
||||
// Don't report error if the file already doesn't exist
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
return Err(FileRepositoryError::IoError(e));
|
||||
}
|
||||
} else {
|
||||
debug!("File physically deleted successfully");
|
||||
}
|
||||
|
||||
// Invalidate cache for this file
|
||||
debug!("Invalidating cache for file: {}", file_path.display());
|
||||
self.metadata_cache().invalidate(&file_path).await;
|
||||
} else {
|
||||
debug!("File does not exist physically, only cleaning mappings: {}", file_path.display());
|
||||
}
|
||||
|
||||
// Always remove the ID mapping regardless of whether the file exists
|
||||
debug!("Removing ID mapping: {}", file_id);
|
||||
match self.id_mapping_service().remove_id(file_id).await {
|
||||
Ok(_) => debug!("ID mapping successfully removed"),
|
||||
Err(e) => {
|
||||
error!("Error removing file mapping: {}", e);
|
||||
// Only return error for critical mapping errors, otherwise continue
|
||||
if format!("{}", e).contains("not found") {
|
||||
debug!("ID mapping not found, ignoring this error for deletion");
|
||||
} else {
|
||||
return Err(FileRepositoryError::MappingError(format!("Failed to remove mapping: {}", e)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
debug!("File permanently deleted successfully: {}", file_id);
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
// This could happen if the file is already deleted or wasn't properly indexed
|
||||
error!("Error getting file path {}: {:?}", file_id, e);
|
||||
|
||||
// Check if the error is because the ID was not found
|
||||
if format!("{}", e).contains("not found") {
|
||||
debug!("ID not found in mapping, considering deletion successful: {}", file_id);
|
||||
// In this case, we consider the file already deleted
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(FileRepositoryError::IdMappingError(format!("Failed to get file path: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-exports needed for the compiler
|
||||
use crate::domain::repositories::file_repository::FileRepositoryError;
|
||||
@@ -1,103 +1,223 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use tokio::{fs, time};
|
||||
use tokio::fs::File as TokioFile;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use futures::{Stream, StreamExt};
|
||||
use bytes::Bytes;
|
||||
use mime_guess::from_path;
|
||||
use tokio::task;
|
||||
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::application::ports::storage_ports::FileWritePort;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::repositories::file_repository::FileRepositoryResult;
|
||||
use crate::infrastructure::repositories::file_metadata_manager::{FileMetadataManager, MetadataError};
|
||||
use crate::infrastructure::repositories::file_path_resolver::FilePathResolver;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::application::services::storage_mediator::StorageMediator;
|
||||
use crate::infrastructure::repositories::repository_errors::{FileRepositoryResult, FileRepositoryError};
|
||||
use crate::infrastructure::services::file_system_utils::FileSystemUtils;
|
||||
use crate::application::ports::cache_ports::MetadataCachePort;
|
||||
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||
use crate::application::services::storage_mediator::StorageMediator;
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::config::AppConfig;
|
||||
|
||||
/// Implementación de repositorio para operaciones de escritura de archivos
|
||||
/// Implementación de repositorio para operaciones de **escritura** de archivos.
|
||||
///
|
||||
/// Implementa `FileWritePort`:
|
||||
/// save_file, save_file_from_stream, move_file, delete_file,
|
||||
/// update_file_content, register_file_deferred.
|
||||
pub struct FileFsWriteRepository {
|
||||
metadata_manager: Arc<FileMetadataManager>,
|
||||
path_resolver: Arc<FilePathResolver>,
|
||||
root_path: PathBuf,
|
||||
storage_mediator: Arc<dyn StorageMediator>,
|
||||
id_mapping_service: Arc<dyn crate::application::ports::outbound::IdMappingPort>,
|
||||
path_service: Arc<PathService>,
|
||||
metadata_cache: Arc<dyn MetadataCachePort>,
|
||||
config: AppConfig,
|
||||
parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
}
|
||||
|
||||
impl FileFsWriteRepository {
|
||||
/// Crea un nuevo repositorio de escritura de archivos
|
||||
/// Constructor completo con todas las dependencias.
|
||||
pub fn new(
|
||||
_root_path: PathBuf,
|
||||
metadata_manager: Arc<FileMetadataManager>,
|
||||
path_resolver: Arc<FilePathResolver>,
|
||||
_storage_mediator: Arc<dyn StorageMediator>,
|
||||
root_path: PathBuf,
|
||||
storage_mediator: Arc<dyn StorageMediator>,
|
||||
id_mapping_service: Arc<dyn crate::application::ports::outbound::IdMappingPort>,
|
||||
path_service: Arc<PathService>,
|
||||
metadata_cache: Arc<dyn MetadataCachePort>,
|
||||
config: AppConfig,
|
||||
_parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
metadata_manager,
|
||||
path_resolver,
|
||||
config,
|
||||
}
|
||||
Self { root_path, storage_mediator, id_mapping_service, path_service, metadata_cache, config, parallel_processor }
|
||||
}
|
||||
|
||||
/// Crea un stub para pruebas
|
||||
|
||||
/// Stub para pruebas (no realiza I/O real).
|
||||
pub fn default_stub() -> Self {
|
||||
Self {
|
||||
metadata_manager: Arc::new(FileMetadataManager::default()),
|
||||
path_resolver: Arc::new(FilePathResolver::default_stub()),
|
||||
root_path: PathBuf::from("./storage"),
|
||||
storage_mediator: Arc::new(
|
||||
crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub(),
|
||||
),
|
||||
id_mapping_service: Arc::new(crate::common::stubs::StubIdMappingPort),
|
||||
path_service: Arc::new(PathService::new(PathBuf::from("./storage"))),
|
||||
metadata_cache: Arc::new(
|
||||
crate::infrastructure::services::file_metadata_cache::FileMetadataCache::default()
|
||||
) as Arc<dyn MetadataCachePort>,
|
||||
config: AppConfig::default(),
|
||||
parallel_processor: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea directorios padres si es necesario, con sincronización
|
||||
|
||||
// ─── helpers ─────────────────────────────────────────────
|
||||
|
||||
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
self.path_service.resolve_path(storage_path)
|
||||
}
|
||||
|
||||
async fn ensure_parent_directory(&self, abs_path: &PathBuf) -> FileRepositoryResult<()> {
|
||||
if let Some(parent) = abs_path.parent() {
|
||||
tokio::time::timeout(
|
||||
time::timeout(
|
||||
self.config.timeouts.dir_timeout(),
|
||||
FileSystemUtils::create_dir_with_sync(parent)
|
||||
FileSystemUtils::create_dir_with_sync(parent),
|
||||
).await
|
||||
.map_err(|_| crate::domain::repositories::file_repository::FileRepositoryError::Timeout(
|
||||
format!("Timeout creating parent directory: {}", parent.display())
|
||||
))?
|
||||
.map_err(crate::domain::repositories::file_repository::FileRepositoryError::IoError)?;
|
||||
.map_err(|_| FileRepositoryError::StorageError(format!("Timeout creating dir: {}", parent.display())))?
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Crea una entidad de archivo a partir de metadatos
|
||||
async fn create_file_entity(
|
||||
&self,
|
||||
id: String,
|
||||
name: String,
|
||||
storage_path: StoragePath,
|
||||
size: u64,
|
||||
mime_type: String,
|
||||
folder_id: Option<String>,
|
||||
created_at: Option<u64>,
|
||||
modified_at: Option<u64>,
|
||||
) -> FileRepositoryResult<File> {
|
||||
// If timestamps are provided, use them; otherwise, let File::new create default timestamps
|
||||
if let (Some(created), Some(modified)) = (created_at, modified_at) {
|
||||
File::with_timestamps(
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
size,
|
||||
mime_type,
|
||||
folder_id,
|
||||
created,
|
||||
modified,
|
||||
)
|
||||
.map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string()))
|
||||
} else {
|
||||
File::new(
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
size,
|
||||
mime_type,
|
||||
folder_id,
|
||||
)
|
||||
.map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string()))
|
||||
|
||||
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> FileRepositoryResult<bool> {
|
||||
let abs = self.resolve_storage_path(storage_path);
|
||||
if let Some(is_file) = self.metadata_cache.is_file(&abs).await {
|
||||
return Ok(is_file);
|
||||
}
|
||||
match time::timeout(self.config.timeouts.file_timeout(), fs::metadata(&abs)).await {
|
||||
Ok(Ok(m)) => {
|
||||
let _ = self.metadata_cache.refresh_metadata(&abs).await;
|
||||
Ok(m.is_file())
|
||||
}
|
||||
Ok(Err(_)) => Ok(false),
|
||||
Err(_) => Err(FileRepositoryError::StorageError(format!("Timeout: {}", abs.display()))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_file_metadata_raw(&self, abs_path: &PathBuf) -> FileRepositoryResult<(u64, u64, u64)> {
|
||||
if let Some(cached) = self.metadata_cache.get_metadata(abs_path).await {
|
||||
if let (Some(s), Some(c), Some(m)) = (cached.size, cached.created_at, cached.modified_at) {
|
||||
return Ok((s, c, m));
|
||||
}
|
||||
}
|
||||
let meta = time::timeout(self.config.timeouts.file_timeout(), fs::metadata(abs_path))
|
||||
.await
|
||||
.map_err(|_| FileRepositoryError::StorageError(format!("Timeout: {}", abs_path.display())))?
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
let s = meta.len();
|
||||
let c = meta.created().map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()).unwrap_or(0);
|
||||
let m = meta.modified().map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()).unwrap_or(0);
|
||||
let _ = self.metadata_cache.refresh_metadata(abs_path).await;
|
||||
Ok((s, c, m))
|
||||
}
|
||||
|
||||
/// Resolve folder to StoragePath
|
||||
async fn resolve_folder_path(&self, folder_id: &Option<String>) -> StoragePath {
|
||||
match folder_id {
|
||||
Some(id) => match self.storage_mediator.get_folder_path(id).await {
|
||||
Ok(path) => {
|
||||
let lossy = path.to_string_lossy().to_string();
|
||||
let folder_name = path.file_name().and_then(|f| f.to_str()).unwrap_or(&lossy);
|
||||
StoragePath::from_string(folder_name)
|
||||
}
|
||||
Err(_) => StoragePath::root(),
|
||||
},
|
||||
None => StoragePath::root(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate unique file path avoiding name collisions.
|
||||
async fn unique_file_path(
|
||||
&self,
|
||||
folder_path: &StoragePath,
|
||||
name: &str,
|
||||
) -> FileRepositoryResult<(StoragePath, String)> {
|
||||
let mut file_path = folder_path.join(name);
|
||||
let mut actual_name = name.to_string();
|
||||
let mut counter = 1;
|
||||
while self.file_exists_at_storage_path(&file_path).await? {
|
||||
let (stem, ext) = if let Some(dot) = name.rfind('.') {
|
||||
(name[..dot].to_string(), name[dot..].to_string())
|
||||
} else {
|
||||
(name.to_string(), String::new())
|
||||
};
|
||||
actual_name = format!("{}_{}{}", stem, counter, ext);
|
||||
file_path = folder_path.join(&actual_name);
|
||||
counter += 1;
|
||||
}
|
||||
Ok((file_path, actual_name))
|
||||
}
|
||||
|
||||
async fn delete_file_non_blocking(&self, abs_path: PathBuf) -> FileRepositoryResult<()> {
|
||||
let file_size = match fs::metadata(&abs_path).await {
|
||||
Ok(m) => m.len(),
|
||||
Err(_) => 0,
|
||||
};
|
||||
if self.config.resources.is_large_file(file_size) {
|
||||
task::spawn_blocking(move || { let _ = std::fs::remove_file(&abs_path); })
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::Other(e.to_string()))?;
|
||||
} else {
|
||||
time::timeout(self.config.timeouts.file_timeout(), fs::remove_file(&abs_path))
|
||||
.await
|
||||
.map_err(|_| FileRepositoryError::StorageError("Timeout deleting file".into()))?
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Persist ID mapping with retry + verification.
|
||||
async fn persist_id_mapping(&self, id: &str, expected_path: &str) -> FileRepositoryResult<()> {
|
||||
for attempt in 1..=3 {
|
||||
match self.id_mapping_service.save_changes().await {
|
||||
Ok(_) => {
|
||||
if let Ok(verified) = self.id_mapping_service.get_path_by_id(id).await {
|
||||
if verified.to_string() == expected_path {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if attempt == 3 {
|
||||
return Err(FileRepositoryError::Other("Failed to verify ID mapping after 3 attempts".into()));
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
Err(e) if attempt < 3 => {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
tracing::warn!("ID mapping save retry {}: {}", attempt, e);
|
||||
}
|
||||
Err(e) => return Err(FileRepositoryError::Other(format!("Save ID mapping failed: {}", e))),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for FileFsWriteRepository {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
root_path: self.root_path.clone(),
|
||||
storage_mediator: self.storage_mediator.clone(),
|
||||
id_mapping_service: self.id_mapping_service.clone(),
|
||||
path_service: self.path_service.clone(),
|
||||
metadata_cache: self.metadata_cache.clone(),
|
||||
config: self.config.clone(),
|
||||
parallel_processor: self.parallel_processor.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_repo_err(e: FileRepositoryError) -> DomainError {
|
||||
match e {
|
||||
FileRepositoryError::NotFound(m) => DomainError::not_found("File", m),
|
||||
FileRepositoryError::AlreadyExists(m) => DomainError::already_exists("File", m),
|
||||
FileRepositoryError::StorageError(m) => DomainError::internal_error("File", m),
|
||||
other => DomainError::internal_error("File", other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,103 +230,288 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> Result<File, DomainError> {
|
||||
// Generate a unique ID for the file
|
||||
let file_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
// Calculate the storage path for this file
|
||||
let storage_path = match &folder_id {
|
||||
Some(folder_id) => {
|
||||
StoragePath::from_string(
|
||||
&format!("/{}/{}", folder_id, name)
|
||||
)
|
||||
},
|
||||
None => {
|
||||
StoragePath::from_string(
|
||||
&format!("/{}", name)
|
||||
)
|
||||
let folder_path = self.resolve_folder_path(&folder_id).await;
|
||||
let (file_storage_path, actual_name) = self.unique_file_path(&folder_path, &name).await.map_err(map_repo_err)?;
|
||||
let abs_path = self.resolve_storage_path(&file_storage_path);
|
||||
self.ensure_parent_directory(&abs_path).await.map_err(map_repo_err)?;
|
||||
|
||||
let content_size = content.len() as u64;
|
||||
|
||||
// Write strategy based on file size
|
||||
if self.config.resources.needs_parallel_processing(content_size, &self.config.concurrency) {
|
||||
if let Some(proc) = &self.parallel_processor {
|
||||
proc.write_file_parallel(&abs_path, &content).await.map_err(map_repo_err)?;
|
||||
} else {
|
||||
let proc = ParallelFileProcessor::new(self.config.clone());
|
||||
proc.write_file_parallel(&abs_path, &content).await.map_err(map_repo_err)?;
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve the absolute path on disk
|
||||
let abs_path = self.path_resolver.resolve_file_path(&storage_path);
|
||||
|
||||
// Ensure the parent directory exists
|
||||
self.ensure_parent_directory(&abs_path).await
|
||||
.map_err(|e| DomainError::internal_error("File system", e.to_string()))?;
|
||||
|
||||
// Write the file to disk using atomic write with fsync
|
||||
tokio::time::timeout(
|
||||
self.config.timeouts.file_write_timeout(),
|
||||
FileSystemUtils::atomic_write(&abs_path, &content)
|
||||
).await
|
||||
.map_err(|_| DomainError::internal_error(
|
||||
"File write",
|
||||
format!("Timeout writing file: {}", abs_path.display())
|
||||
))?
|
||||
.map_err(|e| DomainError::internal_error(
|
||||
"File system",
|
||||
format!("Error writing file: {} - {}", abs_path.display(), e)
|
||||
))?;
|
||||
|
||||
// Create and return a File entity
|
||||
let size = content.len() as u64;
|
||||
let file = self.create_file_entity(
|
||||
file_id,
|
||||
name,
|
||||
storage_path,
|
||||
size,
|
||||
content_type,
|
||||
folder_id,
|
||||
None,
|
||||
None,
|
||||
).await
|
||||
.map_err(|e| DomainError::internal_error("File entity creation", e.to_string()))?;
|
||||
|
||||
// Save metadata
|
||||
self.metadata_manager.update_file_metadata(&file)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
MetadataError::IoError(e) => DomainError::internal_error("File metadata", e.to_string()),
|
||||
MetadataError::Timeout(msg) => DomainError::internal_error("File metadata", msg),
|
||||
MetadataError::Unavailable(msg) => DomainError::not_found("File metadata", msg)
|
||||
})?;
|
||||
|
||||
tracing::info!("File saved successfully: {} (ID: {})", file.name(), file.id());
|
||||
} else if content_size > self.config.resources.large_file_threshold_mb * 1024 * 1024 {
|
||||
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::create(&abs_path))
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout creating file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let chunk_size = self.config.resources.chunk_size_bytes;
|
||||
for chunk in content.chunks(chunk_size) {
|
||||
fh.write_all(chunk).await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
}
|
||||
fh.flush().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
} else {
|
||||
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::create(&abs_path))
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout creating file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.write_all(&content).await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.flush().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
}
|
||||
|
||||
let (size, created_at, modified_at) = self.get_file_metadata_raw(&abs_path).await.map_err(map_repo_err)?;
|
||||
let mime = if content_type.is_empty() { from_path(&abs_path).first_or_octet_stream().to_string() } else { content_type };
|
||||
let id = self.id_mapping_service.get_or_create_id(&file_storage_path).await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let path_string = file_storage_path.to_string();
|
||||
|
||||
let file = File::with_timestamps(id.clone(), actual_name, file_storage_path, size, mime, folder_id, created_at, modified_at)
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
self.persist_id_mapping(&id, &path_string).await.map_err(map_repo_err)?;
|
||||
if let Some(parent) = abs_path.parent() {
|
||||
self.metadata_cache.invalidate_directory(parent).await;
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
async fn move_file(&self, _file_id: &str, _target_folder_id: Option<String>) -> Result<File, DomainError> {
|
||||
// Implementación real debe mover el archivo a otra carpeta
|
||||
// Por ahora, devolvemos un error
|
||||
Err(DomainError::internal_error("File move", "Move functionality not yet implemented"))
|
||||
|
||||
async fn save_file_from_stream(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
mut stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
||||
) -> Result<File, DomainError> {
|
||||
let folder_path = self.resolve_folder_path(&folder_id).await;
|
||||
let (file_storage_path, actual_name) = self.unique_file_path(&folder_path, &name).await.map_err(map_repo_err)?;
|
||||
let abs_path = self.resolve_storage_path(&file_storage_path);
|
||||
self.ensure_parent_directory(&abs_path).await.map_err(map_repo_err)?;
|
||||
|
||||
let temp_path = abs_path.with_extension("tmp.upload");
|
||||
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::create(&temp_path))
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout creating temp file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
let mut total_bytes: u64 = 0;
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let chunk = chunk_result.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.write_all(&chunk).await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
total_bytes += chunk.len() as u64;
|
||||
}
|
||||
fh.flush().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.sync_all().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
drop(fh);
|
||||
|
||||
// Atomic rename
|
||||
fs::rename(&temp_path, &abs_path).await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
let (size, created_at, modified_at) = self.get_file_metadata_raw(&abs_path).await.map_err(map_repo_err)?;
|
||||
let mime = if content_type.is_empty() { from_path(&abs_path).first_or_octet_stream().to_string() } else { content_type };
|
||||
let id = self.id_mapping_service.get_or_create_id(&file_storage_path).await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let path_string = file_storage_path.to_string();
|
||||
let log_name = actual_name.clone();
|
||||
|
||||
let file = File::with_timestamps(id.clone(), actual_name, file_storage_path, size, mime, folder_id, created_at, modified_at)
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
self.persist_id_mapping(&id, &path_string).await.map_err(map_repo_err)?;
|
||||
if let Some(parent) = abs_path.parent() {
|
||||
self.metadata_cache.invalidate_directory(parent).await;
|
||||
}
|
||||
tracing::info!("✅ STREAMING UPLOAD COMPLETE: {} ({} bytes)", log_name, total_bytes);
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
async fn delete_file(&self, _id: &str) -> Result<(), DomainError> {
|
||||
// Por ahora, devolvemos OK simulando éxito
|
||||
// En una implementación real, buscaríamos el archivo por ID y lo eliminaríamos
|
||||
tracing::info!("File deletion simulated successfully");
|
||||
|
||||
async fn move_file(
|
||||
&self,
|
||||
file_id: &str,
|
||||
target_folder_id: Option<String>,
|
||||
) -> Result<File, DomainError> {
|
||||
// Get original file
|
||||
let original_path = self.id_mapping_service.get_path_by_id(file_id).await?;
|
||||
let old_abs = self.resolve_storage_path(&original_path);
|
||||
if !old_abs.exists() || !old_abs.is_file() {
|
||||
return Err(DomainError::not_found("File", file_id.to_string()));
|
||||
}
|
||||
let (size, created_at, modified_at) = self.get_file_metadata_raw(&old_abs).await.map_err(map_repo_err)?;
|
||||
let name = original_path.file_name()
|
||||
.ok_or_else(|| DomainError::internal_error("File", "Invalid path"))?;
|
||||
let mime = from_path(&old_abs).first_or_octet_stream().to_string();
|
||||
|
||||
// Build target path
|
||||
let target_folder_path = self.resolve_folder_path(&target_folder_id).await;
|
||||
let new_storage_path = target_folder_path.join(&name);
|
||||
if self.file_exists_at_storage_path(&new_storage_path).await.map_err(map_repo_err)? {
|
||||
return Err(DomainError::already_exists("File",
|
||||
format!("File already exists at {}", new_storage_path.to_string())));
|
||||
}
|
||||
let new_abs = self.resolve_storage_path(&new_storage_path);
|
||||
self.ensure_parent_directory(&new_abs).await.map_err(map_repo_err)?;
|
||||
|
||||
// Rename
|
||||
time::timeout(
|
||||
self.config.timeouts.file_timeout(),
|
||||
FileSystemUtils::rename_with_sync(&old_abs, &new_abs),
|
||||
).await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout moving file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
// Update mapping
|
||||
self.id_mapping_service.update_path(file_id, &new_storage_path).await?;
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
File::with_timestamps(file_id.to_string(), name, new_storage_path, size, mime, target_folder_id, created_at, modified_at)
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))
|
||||
}
|
||||
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(id).await?;
|
||||
let abs_path = self.resolve_storage_path(&storage_path);
|
||||
|
||||
self.metadata_cache.invalidate(&abs_path).await;
|
||||
if let Some(parent) = abs_path.parent() {
|
||||
self.metadata_cache.invalidate_directory(parent).await;
|
||||
}
|
||||
|
||||
self.delete_file_non_blocking(abs_path).await.map_err(map_repo_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_folder_details(&self, folder_id: &str) -> Result<File, DomainError> {
|
||||
// Fetch the folder information from the metadata manager
|
||||
match self.metadata_manager.get_folder_by_id(folder_id).await {
|
||||
Ok(folder) => Ok(folder),
|
||||
Err(err) => {
|
||||
tracing::warn!("Error getting folder details for ID {}: {}", folder_id, err);
|
||||
Err(DomainError::not_found("Folder", folder_id.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError> {
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(file_id).await?;
|
||||
let physical_path = self.resolve_storage_path(&storage_path);
|
||||
|
||||
FileSystemUtils::atomic_write(&physical_path, &content)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
// Refresh cache
|
||||
let _ = self.metadata_cache.refresh_metadata(&physical_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_folder_path_str(&self, folder_id: &str) -> Result<String, DomainError> {
|
||||
// Fetch the folder information
|
||||
let folder = self.get_folder_details(folder_id).await?;
|
||||
|
||||
// Convert StoragePath to string
|
||||
let path_str = folder.storage_path().to_string();
|
||||
|
||||
tracing::debug!("Resolved folder path for ID {}: {}", folder_id, path_str);
|
||||
Ok(path_str)
|
||||
|
||||
async fn register_file_deferred(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
size: u64,
|
||||
) -> Result<(File, PathBuf), DomainError> {
|
||||
let folder_path = self.resolve_folder_path(&folder_id).await;
|
||||
let (file_storage_path, actual_name) = self.unique_file_path(&folder_path, &name).await.map_err(map_repo_err)?;
|
||||
let abs_path = self.resolve_storage_path(&file_storage_path);
|
||||
self.ensure_parent_directory(&abs_path).await.map_err(map_repo_err)?;
|
||||
|
||||
let mime = if content_type.is_empty() {
|
||||
from_path(&abs_path).first_or_octet_stream().to_string()
|
||||
} else {
|
||||
content_type
|
||||
};
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
let id = self.id_mapping_service.get_or_create_id(&file_storage_path).await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
let file = File::with_timestamps(id.clone(), actual_name, file_storage_path, size, mime, folder_id, now, now)
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
tracing::debug!("⚡ Registered deferred file: {} -> {:?}", id, abs_path);
|
||||
Ok((file, abs_path))
|
||||
}
|
||||
|
||||
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError> {
|
||||
// Get the file's current path
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(file_id).await?;
|
||||
let abs_path = self.resolve_storage_path(&storage_path);
|
||||
|
||||
if !abs_path.exists() || !abs_path.is_file() {
|
||||
return Err(DomainError::not_found("File", file_id.to_string()));
|
||||
}
|
||||
|
||||
// Create trash directory
|
||||
let trash_dir = self.root_path.join(".trash").join("files");
|
||||
fs::create_dir_all(&trash_dir).await
|
||||
.map_err(|e| DomainError::internal_error("File", format!("Failed to create trash dir: {}", e)))?;
|
||||
|
||||
// Move file to trash
|
||||
let trash_path = trash_dir.join(file_id);
|
||||
fs::rename(&abs_path, &trash_path).await
|
||||
.map_err(|e| DomainError::internal_error("File", format!("Failed to move file to trash: {}", e)))?;
|
||||
|
||||
// Update mapping to trash location
|
||||
let trash_storage_path = StoragePath::from_string(&format!(".trash/files/{}", file_id));
|
||||
self.id_mapping_service.update_path(file_id, &trash_storage_path).await?;
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
// Invalidate cache
|
||||
self.metadata_cache.invalidate(&abs_path).await;
|
||||
if let Some(parent) = abs_path.parent() {
|
||||
self.metadata_cache.invalidate_directory(parent).await;
|
||||
}
|
||||
|
||||
tracing::debug!("File moved to trash: {} -> {}", file_id, trash_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError> {
|
||||
// Get current path (should be in trash)
|
||||
let current_storage_path = self.id_mapping_service.get_path_by_id(file_id).await?;
|
||||
let current_abs_path = self.resolve_storage_path(¤t_storage_path);
|
||||
|
||||
if !current_abs_path.exists() {
|
||||
return Err(DomainError::not_found("File", format!("File {} not found in trash", file_id)));
|
||||
}
|
||||
|
||||
// Ensure parent directory exists for original location
|
||||
let original_storage_path = StoragePath::from_string(original_path);
|
||||
let original_abs_path = self.resolve_storage_path(&original_storage_path);
|
||||
if let Some(parent) = original_abs_path.parent() {
|
||||
fs::create_dir_all(parent).await
|
||||
.map_err(|e| DomainError::internal_error("File", format!("Failed to create parent dir: {}", e)))?;
|
||||
}
|
||||
|
||||
// Move file back to original location
|
||||
fs::rename(¤t_abs_path, &original_abs_path).await
|
||||
.map_err(|e| DomainError::internal_error("File", format!("Failed to restore file: {}", e)))?;
|
||||
|
||||
// Update mapping back to original path
|
||||
self.id_mapping_service.update_path(file_id, &original_storage_path).await?;
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
tracing::debug!("File restored from trash: {} -> {}", file_id, original_abs_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError> {
|
||||
// Get current path (could be in trash or original location)
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(file_id).await?;
|
||||
let abs_path = self.resolve_storage_path(&storage_path);
|
||||
|
||||
// Delete the physical file if it exists
|
||||
if abs_path.exists() {
|
||||
self.delete_file_non_blocking(abs_path.clone()).await.map_err(map_repo_err)?;
|
||||
}
|
||||
|
||||
// Remove ID mapping
|
||||
self.id_mapping_service.remove_id(file_id).await?;
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
// Invalidate cache
|
||||
self.metadata_cache.invalidate(&abs_path).await;
|
||||
|
||||
tracing::debug!("File permanently deleted: {}", file_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::time;
|
||||
use tokio::fs;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::infrastructure::services::file_metadata_cache::{FileMetadataCache, CacheEntryType, FileMetadata};
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Gestor de metadatos de archivos que encapsula la lógica de caché
|
||||
pub struct FileMetadataManager {
|
||||
metadata_cache: Arc<FileMetadataCache>,
|
||||
config: AppConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MetadataError {
|
||||
#[error("Error de E/S al acceder a los metadatos: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Timeout al acceder a los metadatos: {0}")]
|
||||
Timeout(String),
|
||||
|
||||
#[error("Metadatos no disponibles: {0}")]
|
||||
Unavailable(String),
|
||||
}
|
||||
|
||||
impl From<MetadataError> for DomainError {
|
||||
fn from(err: MetadataError) -> Self {
|
||||
match err {
|
||||
MetadataError::IoError(e) => DomainError::internal_error("FileMetadata", e.to_string()),
|
||||
MetadataError::Timeout(msg) => DomainError::internal_error("FileMetadata", msg),
|
||||
MetadataError::Unavailable(msg) => DomainError::not_found("FileMetadata", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileMetadataManager {
|
||||
/// Crea un nuevo gestor de metadatos
|
||||
pub fn new(metadata_cache: Arc<FileMetadataCache>, config: AppConfig) -> Self {
|
||||
Self {
|
||||
metadata_cache,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un gestor por defecto para pruebas
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
metadata_cache: Arc::new(FileMetadataCache::default()),
|
||||
config: AppConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Comprueba si un archivo existe en la ruta especificada con caché
|
||||
pub async fn file_exists(&self, abs_path: &PathBuf) -> Result<bool, MetadataError> {
|
||||
// Intentar obtener del caché avanzado primero
|
||||
if let Some(is_file) = self.metadata_cache.is_file(&abs_path).await {
|
||||
tracing::debug!("Metadata cache hit for existence check: {} - path: {}", is_file, abs_path.display());
|
||||
return Ok(is_file);
|
||||
}
|
||||
|
||||
// Si no está en caché, verificar directamente y actualizar caché
|
||||
tracing::debug!("Metadata cache miss for existence check: {}", abs_path.display());
|
||||
|
||||
// Utilizar timeout para evitar bloqueo
|
||||
match time::timeout(
|
||||
self.config.timeouts.file_timeout(),
|
||||
fs::metadata(&abs_path)
|
||||
).await {
|
||||
Ok(Ok(metadata)) => {
|
||||
let is_file = metadata.is_file();
|
||||
|
||||
// Actualizar la caché con información fresca
|
||||
if let Err(e) = self.metadata_cache.refresh_metadata(&abs_path).await {
|
||||
tracing::warn!("Failed to update cache for {}: {}", abs_path.display(), e);
|
||||
}
|
||||
|
||||
if is_file {
|
||||
tracing::debug!("File exists and is accessible: {}", abs_path.display());
|
||||
Ok(true)
|
||||
} else {
|
||||
tracing::warn!("Path exists but is not a file: {}", abs_path.display());
|
||||
Ok(false)
|
||||
}
|
||||
},
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!("File check failed: {} - {}", abs_path.display(), e);
|
||||
|
||||
// Añadir a caché como no existente
|
||||
let entry_type = CacheEntryType::Unknown;
|
||||
let file_metadata = FileMetadata::new(
|
||||
abs_path.clone(),
|
||||
false,
|
||||
entry_type,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Duration::from_millis(self.config.timeouts.file_operation_ms),
|
||||
);
|
||||
self.metadata_cache.update_cache(file_metadata).await;
|
||||
|
||||
Ok(false)
|
||||
},
|
||||
Err(_) => {
|
||||
tracing::warn!("Timeout checking file metadata: {}", abs_path.display());
|
||||
Err(MetadataError::Timeout(format!("Timeout checking file: {}", abs_path.display())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtiene metadatos de archivo (tamaño, fechas creación/modificación) con caché
|
||||
pub async fn get_file_metadata(&self, abs_path: &PathBuf) -> Result<(u64, u64, u64), MetadataError> {
|
||||
// Intentar obtener de caché primero
|
||||
if let Some(cached_metadata) = self.metadata_cache.get_metadata(abs_path).await {
|
||||
if let (Some(size), Some(created_at), Some(modified_at)) =
|
||||
(cached_metadata.size, cached_metadata.created_at, cached_metadata.modified_at) {
|
||||
tracing::debug!("Using cached metadata for: {}", abs_path.display());
|
||||
return Ok((size, created_at, modified_at));
|
||||
}
|
||||
}
|
||||
|
||||
// Si no está en caché o metadatos incompletos, cargar desde sistema de archivos
|
||||
let metadata = match time::timeout(
|
||||
self.config.timeouts.file_timeout(),
|
||||
fs::metadata(&abs_path)
|
||||
).await {
|
||||
Ok(Ok(metadata)) => metadata,
|
||||
Ok(Err(e)) => return Err(MetadataError::IoError(e)),
|
||||
Err(_) => return Err(MetadataError::Timeout(
|
||||
format!("Timeout getting metadata for: {}", abs_path.display())
|
||||
)),
|
||||
};
|
||||
|
||||
let size = metadata.len();
|
||||
|
||||
// Get creation timestamp
|
||||
let created_at = metadata.created()
|
||||
.map(|time| time.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
.unwrap_or_else(|_| 0);
|
||||
|
||||
// Get modification timestamp
|
||||
let modified_at = metadata.modified()
|
||||
.map(|time| time.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
.unwrap_or_else(|_| 0);
|
||||
|
||||
// Actualizar caché si es posible
|
||||
if let Err(e) = self.metadata_cache.refresh_metadata(abs_path).await {
|
||||
tracing::warn!("Failed to update metadata cache for {}: {}", abs_path.display(), e);
|
||||
}
|
||||
|
||||
Ok((size, created_at, modified_at))
|
||||
}
|
||||
|
||||
/// Invalida la entrada de caché para un archivo
|
||||
pub async fn invalidate(&self, abs_path: &PathBuf) {
|
||||
self.metadata_cache.invalidate(abs_path).await;
|
||||
}
|
||||
|
||||
/// Invalida la entrada de caché para un directorio y su contenido
|
||||
pub async fn invalidate_directory(&self, dir_path: &PathBuf) {
|
||||
self.metadata_cache.invalidate_directory(dir_path).await;
|
||||
}
|
||||
|
||||
/// Actualiza los metadatos de un archivo en la caché
|
||||
pub async fn update_file_metadata(&self, file: &crate::domain::entities::file::File) -> Result<(), MetadataError> {
|
||||
// Crear una ruta absoluta para el archivo
|
||||
let abs_path = PathBuf::from(format!("{}/{}", self.config.storage_path.display(), file.storage_path().to_string()));
|
||||
|
||||
// Crear un objeto FileMetadata
|
||||
let metadata = FileMetadataCache::create_metadata_from_file(file, abs_path.clone());
|
||||
|
||||
// Actualizar la caché
|
||||
self.metadata_cache.update_cache(metadata).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Obtiene información de una carpeta por ID
|
||||
pub async fn get_folder_by_id(&self, folder_id: &str) -> Result<File, MetadataError> {
|
||||
// Implementación simplificada que solo busca en la caché de metadatos
|
||||
// pero que devuelve una estructura mínima para el servicio de uso de almacenamiento
|
||||
|
||||
// En una implementación real, se consultaría un índice persistente
|
||||
// Para esta implementación básica, usaremos un método simplificado
|
||||
|
||||
// Crear un objeto StoragePath mínimo
|
||||
let storage_path = StoragePath::from_string(&format!("/{}", folder_id));
|
||||
|
||||
// Creamos una carpeta con información mínima
|
||||
// Esta implementación es un placeholder - en una situación real
|
||||
// consultaríamos el mapa folder_id -> folder_metadata en el sistema
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
// Verificamos si el nombre contiene información del usuario
|
||||
let folder_name = if folder_id.contains('-') {
|
||||
// Asumimos un formato UUID v4, intentamos usar "Mi Carpeta - username" como nombre
|
||||
format!("Mi Carpeta - usuario")
|
||||
} else {
|
||||
// Si no, usamos el ID como nombre
|
||||
folder_id.to_string()
|
||||
};
|
||||
|
||||
let folder = File::new_folder(
|
||||
folder_id.to_string(),
|
||||
folder_name,
|
||||
storage_path,
|
||||
None, // parent_id
|
||||
now, // created_at
|
||||
now, // updated_at
|
||||
)
|
||||
.map_err(|e| MetadataError::Unavailable(format!("Error creating folder entity: {}", e)))?;
|
||||
|
||||
Ok(folder)
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
use crate::application::services::storage_mediator::StorageMediator;
|
||||
// use crate::application::ports::outbound::IdMappingPort;
|
||||
use crate::domain::repositories::file_repository::FileRepositoryError;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::application::ports::storage_ports::FilePathResolutionPort;
|
||||
|
||||
/// Resuelve rutas de archivos y gestiona el mapeo de IDs a rutas
|
||||
pub struct FilePathResolver {
|
||||
path_service: Arc<PathService>,
|
||||
storage_mediator: Arc<dyn StorageMediator>,
|
||||
id_mapping_service: Arc<dyn crate::application::ports::outbound::IdMappingPort>,
|
||||
}
|
||||
|
||||
impl FilePathResolver {
|
||||
/// Crea un nuevo resolver de rutas
|
||||
pub fn new(
|
||||
path_service: Arc<PathService>,
|
||||
storage_mediator: Arc<dyn StorageMediator>,
|
||||
id_mapping_service: Arc<dyn crate::application::ports::outbound::IdMappingPort>,
|
||||
) -> Self {
|
||||
Self {
|
||||
path_service,
|
||||
storage_mediator,
|
||||
id_mapping_service,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un resolver de rutas de prueba
|
||||
pub fn default_stub() -> Self {
|
||||
let path_service = Arc::new(PathService::new(PathBuf::from("./storage")));
|
||||
|
||||
// Create dummy implementation of IdMappingPort
|
||||
struct DummyIdMappingService;
|
||||
#[async_trait::async_trait]
|
||||
impl crate::application::ports::outbound::IdMappingPort for DummyIdMappingService {
|
||||
async fn get_or_create_id(&self, _path: &StoragePath) -> Result<String, DomainError> {
|
||||
Ok("dummy-id".to_string())
|
||||
}
|
||||
|
||||
async fn get_path_by_id(&self, _id: &str) -> Result<StoragePath, DomainError> {
|
||||
Ok(StoragePath::from_string("/"))
|
||||
}
|
||||
|
||||
async fn update_path(&self, _id: &str, _new_path: &StoragePath) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_id(&self, _id: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_changes(&self) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
path_service: path_service.clone(),
|
||||
storage_mediator: Arc::new(crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub()),
|
||||
id_mapping_service: Arc::new(DummyIdMappingService) as Arc<dyn crate::application::ports::outbound::IdMappingPort>,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resuelve una ruta de dominio a una ruta física absoluta
|
||||
pub fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
self.path_service.resolve_path(storage_path)
|
||||
}
|
||||
|
||||
/// Resuelve la ruta de un archivo (alias para resolve_storage_path)
|
||||
pub fn resolve_file_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
self.resolve_storage_path(storage_path)
|
||||
}
|
||||
|
||||
/// Resuelve una ruta PathBuf a una ruta física absoluta (legacy)
|
||||
pub fn resolve_legacy_path(&self, relative_path: &std::path::Path) -> PathBuf {
|
||||
self.storage_mediator.resolve_path(relative_path)
|
||||
}
|
||||
|
||||
/// Obtiene la ruta de un archivo por su ID
|
||||
pub async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, FileRepositoryError> {
|
||||
self.id_mapping_service.get_path_by_id(id).await
|
||||
.map_err(|e| FileRepositoryError::IdMappingError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Actualiza la ruta para un ID existente
|
||||
pub async fn update_path(&self, id: &str, storage_path: &StoragePath) -> Result<(), FileRepositoryError> {
|
||||
self.id_mapping_service.update_path(id, storage_path).await
|
||||
.map_err(|e| FileRepositoryError::IdMappingError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Obtiene o crea un ID para una ruta
|
||||
pub async fn get_or_create_id(&self, storage_path: &StoragePath) -> Result<String, FileRepositoryError> {
|
||||
self.id_mapping_service.get_or_create_id(storage_path).await
|
||||
.map_err(|e| FileRepositoryError::IdMappingError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Elimina un ID del mapeo
|
||||
pub async fn remove_id(&self, id: &str) -> Result<(), FileRepositoryError> {
|
||||
self.id_mapping_service.remove_id(id).await
|
||||
.map_err(|e| FileRepositoryError::IdMappingError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Guarda cambios pendientes
|
||||
pub async fn save_changes(&self) -> Result<(), FileRepositoryError> {
|
||||
self.id_mapping_service.save_changes().await
|
||||
.map_err(|e| FileRepositoryError::IdMappingError(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
// Implementación de FilePathResolutionPort
|
||||
#[async_trait]
|
||||
impl FilePathResolutionPort for FilePathResolver {
|
||||
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError> {
|
||||
self.get_path_by_id(id).await
|
||||
.map_err(|e| match e {
|
||||
FileRepositoryError::NotFound(id) => DomainError::not_found("File", id),
|
||||
FileRepositoryError::IoError(e) => DomainError::internal_error("FilePath", e.to_string()),
|
||||
FileRepositoryError::Timeout(msg) => DomainError::internal_error("FilePath", msg),
|
||||
_ => DomainError::internal_error("FilePath", e.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
self.resolve_storage_path(storage_path)
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,17 @@ use std::time::Duration;
|
||||
use async_trait::async_trait;
|
||||
use tokio::fs;
|
||||
use tokio::time::timeout;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::domain::entities::folder::{Folder, FolderError};
|
||||
use crate::domain::repositories::folder_repository::{
|
||||
FolderRepository, FolderRepositoryError, FolderRepositoryResult
|
||||
use crate::infrastructure::repositories::repository_errors::{
|
||||
FolderRepositoryError, FolderRepositoryResult
|
||||
};
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
// use crate::application::ports::outbound::IdMappingPort;
|
||||
use crate::infrastructure::services::id_mapping_service::{IdMappingService, IdMappingError};
|
||||
use crate::application::services::storage_mediator::StorageMediator;
|
||||
use crate::application::ports::outbound::FolderStoragePort;
|
||||
use crate::domain::repositories::folder_repository::FolderRepository;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
// To be able to use streams in the list_folders function
|
||||
@@ -88,7 +87,7 @@ impl FolderFsRepository {
|
||||
|
||||
match read_dir_result {
|
||||
Ok(result) => {
|
||||
let mut entries = result.map_err(FolderRepositoryError::IoError)?;
|
||||
let mut entries = result.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?;
|
||||
let mut count = 0;
|
||||
|
||||
// Count entries manually
|
||||
@@ -111,20 +110,22 @@ impl FolderFsRepository {
|
||||
self.path_service.resolve_path(storage_path)
|
||||
}
|
||||
|
||||
/// Resolves a legacy PathBuf to an absolute filesystem path
|
||||
fn resolve_legacy_path(&self, relative_path: &std::path::Path) -> PathBuf {
|
||||
self.storage_mediator.resolve_path(relative_path)
|
||||
}
|
||||
|
||||
/// Returns a reference to the ID mapping service
|
||||
pub fn id_mapping_service(&self) -> &Arc<dyn crate::application::ports::outbound::IdMappingPort> {
|
||||
&self.id_mapping_service
|
||||
}
|
||||
|
||||
/// Gets the storage path for a folder by its ID (internal helper)
|
||||
async fn _get_folder_storage_path(&self, id: &str) -> FolderRepositoryResult<StoragePath> {
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(id).await
|
||||
.map_err(FolderRepositoryError::from)?;
|
||||
Ok(storage_path)
|
||||
}
|
||||
|
||||
/// Gets a folder path from the ID mapping service
|
||||
pub async fn get_mapped_folder_path(&self, folder_id: &str) -> FolderRepositoryResult<String> {
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(folder_id).await
|
||||
.map_err(|e| FolderRepositoryError::MappingError(format!("Failed to get folder path: {}", e)))?;
|
||||
.map_err(|e| FolderRepositoryError::StorageError(format!("Failed to get folder path: {}", e)))?;
|
||||
Ok(storage_path.to_string())
|
||||
}
|
||||
|
||||
@@ -132,13 +133,13 @@ impl FolderFsRepository {
|
||||
pub async fn update_mapped_folder_path(&self, folder_id: &str, new_path: &PathBuf) -> FolderRepositoryResult<()> {
|
||||
let storage_path = StoragePath::from_string(&new_path.to_string_lossy().to_string());
|
||||
self.id_mapping_service.update_path(folder_id, &storage_path).await
|
||||
.map_err(|e| FolderRepositoryError::MappingError(format!("Failed to update folder path: {}", e)))
|
||||
.map_err(|e| FolderRepositoryError::StorageError(format!("Failed to update folder path: {}", e)))
|
||||
}
|
||||
|
||||
/// Removes a folder ID from the ID mapping service
|
||||
pub async fn remove_mapped_folder_id(&self, folder_id: &str) -> FolderRepositoryResult<()> {
|
||||
self.id_mapping_service.remove_id(folder_id).await
|
||||
.map_err(|e| FolderRepositoryError::MappingError(format!("Failed to remove folder ID: {}", e)))
|
||||
.map_err(|e| FolderRepositoryError::StorageError(format!("Failed to remove folder ID: {}", e)))
|
||||
}
|
||||
|
||||
/// Checks if a folder exists at a given storage path
|
||||
@@ -199,7 +200,7 @@ impl FolderFsRepository {
|
||||
/// Extracts folder metadata from a physical path
|
||||
async fn get_folder_metadata(&self, abs_path: &PathBuf) -> FolderRepositoryResult<(u64, u64)> {
|
||||
let metadata = fs::metadata(&abs_path).await
|
||||
.map_err(FolderRepositoryError::IoError)?;
|
||||
.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
// Get creation timestamp
|
||||
let created_at = metadata.created()
|
||||
@@ -220,43 +221,9 @@ impl From<IdMappingError> for FolderRepositoryError {
|
||||
fn from(err: IdMappingError) -> Self {
|
||||
match err {
|
||||
IdMappingError::NotFound(id) => FolderRepositoryError::NotFound(id),
|
||||
IdMappingError::IoError(e) => FolderRepositoryError::IoError(e),
|
||||
IdMappingError::Timeout(msg) => FolderRepositoryError::Other(format!("Timeout: {}", msg)),
|
||||
_ => FolderRepositoryError::MappingError(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert FolderRepositoryError to DomainError
|
||||
impl From<FolderRepositoryError> for DomainError {
|
||||
fn from(err: FolderRepositoryError) -> Self {
|
||||
match err {
|
||||
FolderRepositoryError::NotFound(id) => {
|
||||
DomainError::not_found("Folder", id)
|
||||
},
|
||||
FolderRepositoryError::AlreadyExists(path) => {
|
||||
DomainError::already_exists("Folder", path)
|
||||
},
|
||||
FolderRepositoryError::InvalidPath(path) => {
|
||||
DomainError::validation_error(format!("Invalid path: {}", path))
|
||||
},
|
||||
FolderRepositoryError::IoError(e) => {
|
||||
DomainError::internal_error("Folder", format!("IO error: {}", e))
|
||||
.with_source(e)
|
||||
},
|
||||
FolderRepositoryError::ValidationError(msg) => {
|
||||
DomainError::validation_error(msg)
|
||||
},
|
||||
FolderRepositoryError::MappingError(msg) => {
|
||||
DomainError::internal_error("Folder", format!("Mapping error: {}", msg))
|
||||
},
|
||||
FolderRepositoryError::Other(msg) => {
|
||||
DomainError::internal_error("Folder", msg)
|
||||
},
|
||||
FolderRepositoryError::OperationNotSupported(msg) => {
|
||||
DomainError::operation_not_supported("Folder", msg)
|
||||
},
|
||||
FolderRepositoryError::DomainError(e) => e,
|
||||
IdMappingError::IoError(e) => FolderRepositoryError::StorageError(e.to_string()),
|
||||
IdMappingError::Timeout(msg) => FolderRepositoryError::StorageError(format!("Timeout: {}", msg)),
|
||||
_ => FolderRepositoryError::StorageError(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,88 +241,20 @@ impl Clone for FolderFsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FolderStoragePort for FolderFsRepository {
|
||||
async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError> {
|
||||
FolderRepository::create_folder(self, name, parent_id).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError> {
|
||||
FolderRepository::get_folder_by_id(self, id).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError> {
|
||||
FolderRepository::get_folder_by_storage_path(self, storage_path).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError> {
|
||||
FolderRepository::list_folders(self, parent_id).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError> {
|
||||
FolderRepository::rename_folder(self, id, new_name).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> Result<Folder, DomainError> {
|
||||
FolderRepository::move_folder(self, id, new_parent_id).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> {
|
||||
FolderRepository::delete_folder(self, id).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
FolderRepository::folder_exists_at_storage_path(self, storage_path).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError> {
|
||||
FolderRepository::get_folder_storage_path(self, id).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn list_folders_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
include_total: bool
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError> {
|
||||
FolderRepository::list_folders_paginated(self, parent_id, offset, limit, include_total)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FolderRepository for FolderFsRepository {
|
||||
#[instrument(skip(self))]
|
||||
async fn move_to_trash(&self, folder_id: &str) -> FolderRepositoryResult<()> {
|
||||
// Use the private implementation from folder_fs_repository_trash.rs
|
||||
self._trash_move_to_trash(folder_id).await
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> FolderRepositoryResult<()> {
|
||||
// Use the private implementation from folder_fs_repository_trash.rs
|
||||
self._trash_restore_from_trash(folder_id, original_path).await
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn delete_folder_permanently(&self, folder_id: &str) -> FolderRepositoryResult<()> {
|
||||
// Use the private implementation from folder_fs_repository_trash.rs
|
||||
self._trash_delete_folder_permanently(folder_id).await
|
||||
}
|
||||
async fn create_folder(&self, name: String, parent_id: Option<String>) -> FolderRepositoryResult<Folder> {
|
||||
async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError> {
|
||||
// Get the parent folder path (if any)
|
||||
let parent_storage_path = match &parent_id {
|
||||
Some(id) => {
|
||||
match self.get_folder_storage_path(id).await {
|
||||
match self._get_folder_storage_path(id).await {
|
||||
Ok(path) => {
|
||||
tracing::info!("Using folder path: {:?} for parent_id: {:?}", path.to_string(), id);
|
||||
Some(path)
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting parent folder: {}", e);
|
||||
return Err(e);
|
||||
return Err(DomainError::from(e));
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -370,27 +269,28 @@ impl FolderRepository for FolderFsRepository {
|
||||
tracing::info!("Creating folder at path: {:?}", folder_storage_path.to_string());
|
||||
|
||||
// Check if folder already exists
|
||||
if self.folder_exists_at_storage_path(&folder_storage_path).await? {
|
||||
return Err(FolderRepositoryError::AlreadyExists(folder_storage_path.to_string()));
|
||||
if self.check_folder_exists_at_storage_path(&folder_storage_path).await.map_err(DomainError::from)? {
|
||||
return Err(DomainError::already_exists("Folder", folder_storage_path.to_string()));
|
||||
}
|
||||
|
||||
// Create the physical directory
|
||||
let abs_path = self.resolve_storage_path(&folder_storage_path);
|
||||
self.create_directory(&abs_path).await
|
||||
.map_err(FolderRepositoryError::IoError)?;
|
||||
.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?;
|
||||
|
||||
// Create and return the folder entity with a persisted ID
|
||||
let id = self.id_mapping_service.get_or_create_id(&folder_storage_path).await?;
|
||||
let id = self.id_mapping_service.get_or_create_id(&folder_storage_path).await
|
||||
.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?;
|
||||
let folder = self.create_folder_entity(
|
||||
id.clone(), // Clone for logging
|
||||
name.clone(), // Clone name for logging
|
||||
folder_storage_path.clone(), // Clone for logging
|
||||
parent_id.clone(), // Clone for logging
|
||||
id.clone(),
|
||||
name.clone(),
|
||||
folder_storage_path.clone(),
|
||||
parent_id.clone(),
|
||||
None,
|
||||
None,
|
||||
).await?;
|
||||
).await.map_err(DomainError::from)?;
|
||||
|
||||
// Ensure ID mapping is persisted - this is critical for later retrieval
|
||||
// Ensure ID mapping is persisted
|
||||
let save_result = self.id_mapping_service.save_changes().await;
|
||||
if let Err(e) = &save_result {
|
||||
tracing::error!("Failed to save ID mapping for folder {}: {}", id, e);
|
||||
@@ -404,38 +304,36 @@ impl FolderRepository for FolderFsRepository {
|
||||
Ok(folder)
|
||||
}
|
||||
|
||||
async fn get_folder_by_id(&self, id: &str) -> FolderRepositoryResult<Folder> {
|
||||
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError> {
|
||||
tracing::debug!("Looking for folder with ID: {}", id);
|
||||
|
||||
// Find path by ID using the mapping service
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(id).await
|
||||
.map_err(FolderRepositoryError::from)?;
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(id).await?;
|
||||
|
||||
// Check if folder exists physically
|
||||
let abs_path = self.resolve_storage_path(&storage_path);
|
||||
if !abs_path.exists() || !abs_path.is_dir() {
|
||||
tracing::error!("Folder not found at path: {}", abs_path.display());
|
||||
return Err(FolderRepositoryError::NotFound(format!("Folder {} not found at {}", id, storage_path.to_string())));
|
||||
return Err(DomainError::not_found("Folder", format!("Folder {} not found at {}", id, storage_path.to_string())));
|
||||
}
|
||||
|
||||
// Get folder metadata
|
||||
let (created_at, modified_at) = self.get_folder_metadata(&abs_path).await?;
|
||||
let (created_at, modified_at) = self.get_folder_metadata(&abs_path).await.map_err(DomainError::from)?;
|
||||
|
||||
// Get folder name from the storage path
|
||||
let name = match storage_path.file_name() {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
tracing::error!("Invalid folder path: {}", storage_path.to_string());
|
||||
return Err(FolderRepositoryError::InvalidPath(storage_path.to_string()));
|
||||
return Err(DomainError::validation_error(format!("Invalid path: {}", storage_path.to_string())));
|
||||
}
|
||||
};
|
||||
|
||||
// Determine parent ID if any
|
||||
let parent = storage_path.parent();
|
||||
let parent_id: Option<String> = if parent.is_none() || parent.as_ref().unwrap().is_empty() {
|
||||
None // Root folder
|
||||
None
|
||||
} else {
|
||||
// Try to get the parent ID from the mapping service
|
||||
match self.id_mapping_service.get_or_create_id(parent.as_ref().unwrap()).await {
|
||||
Ok(pid) => Some(pid),
|
||||
Err(_) => None,
|
||||
@@ -450,32 +348,31 @@ impl FolderRepository for FolderFsRepository {
|
||||
parent_id,
|
||||
Some(created_at),
|
||||
Some(modified_at),
|
||||
).await?;
|
||||
).await.map_err(DomainError::from)?;
|
||||
|
||||
Ok(folder)
|
||||
}
|
||||
|
||||
async fn get_folder_by_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult<Folder> {
|
||||
async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError> {
|
||||
// Check if the physical directory exists
|
||||
let abs_path = self.resolve_storage_path(storage_path);
|
||||
if !abs_path.exists() || !abs_path.is_dir() {
|
||||
return Err(FolderRepositoryError::NotFound(storage_path.to_string()));
|
||||
return Err(DomainError::not_found("Folder", storage_path.to_string()));
|
||||
}
|
||||
|
||||
// Extract folder name from storage path
|
||||
let name = match storage_path.file_name() {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
return Err(FolderRepositoryError::InvalidPath(storage_path.to_string()));
|
||||
return Err(DomainError::validation_error(format!("Invalid path: {}", storage_path.to_string())));
|
||||
}
|
||||
};
|
||||
|
||||
// Determine parent ID if any
|
||||
let parent = storage_path.parent();
|
||||
let parent_id: Option<String> = if parent.is_none() || parent.as_ref().unwrap().is_empty() {
|
||||
None // Root folder
|
||||
None
|
||||
} else {
|
||||
// Try to get the parent ID from the mapping service
|
||||
match self.id_mapping_service.get_or_create_id(parent.as_ref().unwrap()).await {
|
||||
Ok(pid) => Some(pid),
|
||||
Err(_) => None,
|
||||
@@ -483,7 +380,7 @@ impl FolderRepository for FolderFsRepository {
|
||||
};
|
||||
|
||||
// Get folder metadata
|
||||
let (created_at, modified_at) = self.get_folder_metadata(&abs_path).await?;
|
||||
let (created_at, modified_at) = self.get_folder_metadata(&abs_path).await.map_err(DomainError::from)?;
|
||||
|
||||
// Get or create an ID for this path
|
||||
let id = self.id_mapping_service.get_or_create_id(storage_path).await?;
|
||||
@@ -497,7 +394,7 @@ impl FolderRepository for FolderFsRepository {
|
||||
parent_id,
|
||||
Some(created_at),
|
||||
Some(modified_at),
|
||||
).await?;
|
||||
).await.map_err(DomainError::from)?;
|
||||
|
||||
// Ensure ID mapping is persisted
|
||||
self.id_mapping_service.save_changes().await?;
|
||||
@@ -505,8 +402,8 @@ impl FolderRepository for FolderFsRepository {
|
||||
Ok(folder)
|
||||
}
|
||||
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> FolderRepositoryResult<Vec<Folder>> {
|
||||
use futures::stream::{StreamExt};
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError> {
|
||||
use futures::stream::StreamExt;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
tracing::info!("Listing folders in parent_id: {:?}", parent_id);
|
||||
@@ -514,7 +411,7 @@ impl FolderRepository for FolderFsRepository {
|
||||
// Get the parent storage path
|
||||
let parent_storage_path = match parent_id {
|
||||
Some(id) => {
|
||||
match self.get_folder_storage_path(id).await {
|
||||
match self._get_folder_storage_path(id).await {
|
||||
Ok(path) => {
|
||||
tracing::info!("Found parent folder with path: {:?}", path.to_string());
|
||||
path
|
||||
@@ -538,21 +435,20 @@ impl FolderRepository for FolderFsRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Read the directory with a timeout to avoid indefinite blocking
|
||||
// Read the directory with a timeout
|
||||
let read_dir_timeout = Duration::from_secs(30);
|
||||
let read_dir_result = match timeout(
|
||||
read_dir_timeout,
|
||||
fs::read_dir(&abs_parent_path)
|
||||
).await {
|
||||
Ok(result) => result.map_err(FolderRepositoryError::IoError)?,
|
||||
Ok(result) => result.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?,
|
||||
Err(_) => {
|
||||
return Err(FolderRepositoryError::Other(
|
||||
return Err(DomainError::internal_error("Folder",
|
||||
format!("Timeout reading directory: {}", abs_parent_path.display())
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Process each entry sequentially to avoid async block type issues
|
||||
let mut folders = Vec::new();
|
||||
let mut entries = tokio_stream::wrappers::ReadDirStream::new(read_dir_result);
|
||||
|
||||
@@ -568,27 +464,22 @@ impl FolderRepository for FolderFsRepository {
|
||||
let metadata = match entry.metadata().await {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
tracing::error!("Error getting metadata for {}: {}",
|
||||
entry.path().display(), err);
|
||||
tracing::error!("Error getting metadata for {}: {}", entry.path().display(), err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Skip if not a directory
|
||||
if !metadata.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let folder_name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
// Create the storage path for this folder
|
||||
let folder_storage_path = parent_storage_path.join(&folder_name);
|
||||
|
||||
// Try to get the folder by its storage path with timeout
|
||||
let get_folder_timeout = Duration::from_secs(5);
|
||||
let folder_result = timeout(
|
||||
get_folder_timeout,
|
||||
self.get_folder_by_storage_path(&folder_storage_path)
|
||||
self.get_folder_by_path(&folder_storage_path)
|
||||
).await;
|
||||
|
||||
match folder_result {
|
||||
@@ -609,7 +500,6 @@ impl FolderRepository for FolderFsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// Persist any new ID mappings that were created
|
||||
if let Err(e) = self.id_mapping_service.save_changes().await {
|
||||
tracing::error!("Failed to save ID mappings: {}", e);
|
||||
}
|
||||
@@ -624,21 +514,17 @@ impl FolderRepository for FolderFsRepository {
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
include_total: bool
|
||||
) -> FolderRepositoryResult<(Vec<Folder>, Option<usize>)> {
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError> {
|
||||
use futures::stream::StreamExt;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
tracing::info!("Listing folders in parent_id: {:?} with pagination (offset={}, limit={})",
|
||||
parent_id, offset, limit);
|
||||
|
||||
// Get the parent storage path
|
||||
let parent_storage_path = match parent_id {
|
||||
Some(id) => {
|
||||
match self.get_folder_storage_path(id).await {
|
||||
Ok(path) => {
|
||||
tracing::info!("Found parent folder with path: {:?}", path.to_string());
|
||||
path
|
||||
},
|
||||
match self._get_folder_storage_path(id).await {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting parent folder by ID: {}: {}", id, e);
|
||||
return Ok((Vec::new(), Some(0)));
|
||||
@@ -648,17 +534,12 @@ impl FolderRepository for FolderFsRepository {
|
||||
None => StoragePath::root(),
|
||||
};
|
||||
|
||||
// Get the absolute folder path
|
||||
let abs_parent_path = self.resolve_storage_path(&parent_storage_path);
|
||||
tracing::info!("Absolute parent path: {:?}", abs_parent_path);
|
||||
|
||||
// Ensure the directory exists
|
||||
if !abs_parent_path.exists() || !abs_parent_path.is_dir() {
|
||||
tracing::error!("Directory does not exist or is not a directory: {:?}", abs_parent_path);
|
||||
return Ok((Vec::new(), Some(0)));
|
||||
}
|
||||
|
||||
// Get total count if requested
|
||||
let total_count = if include_total {
|
||||
match self.count_directory_items(&abs_parent_path).await {
|
||||
Ok(count) => Some(count),
|
||||
@@ -671,34 +552,29 @@ impl FolderRepository for FolderFsRepository {
|
||||
None
|
||||
};
|
||||
|
||||
// Read the directory with a timeout to avoid indefinite blocking
|
||||
let read_dir_timeout = Duration::from_secs(30);
|
||||
let read_dir_result = match timeout(
|
||||
read_dir_timeout,
|
||||
fs::read_dir(&abs_parent_path)
|
||||
).await {
|
||||
Ok(result) => result.map_err(FolderRepositoryError::IoError)?,
|
||||
Ok(result) => result.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?,
|
||||
Err(_) => {
|
||||
return Err(FolderRepositoryError::Other(
|
||||
return Err(DomainError::internal_error("Folder",
|
||||
format!("Timeout reading directory: {}", abs_parent_path.display())
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Process entries sequentially to avoid async block typing issues
|
||||
let mut entries = tokio_stream::wrappers::ReadDirStream::new(read_dir_result);
|
||||
let mut folders = Vec::new();
|
||||
let mut current_idx = 0;
|
||||
|
||||
// Loop through entries, applying pagination manually
|
||||
while let Some(entry_result) = entries.next().await {
|
||||
// Skip entries before offset
|
||||
if current_idx < offset {
|
||||
current_idx += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stop after reaching limit
|
||||
if folders.len() >= limit {
|
||||
break;
|
||||
}
|
||||
@@ -712,7 +588,6 @@ impl FolderRepository for FolderFsRepository {
|
||||
}
|
||||
};
|
||||
|
||||
// Check if it's a directory
|
||||
let file_type = match entry.file_type().await {
|
||||
Ok(ft) => ft,
|
||||
Err(e) => {
|
||||
@@ -727,7 +602,6 @@ impl FolderRepository for FolderFsRepository {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the path and convert to StoragePath
|
||||
let path = entry.path();
|
||||
let rel_path = match path.strip_prefix(&self.root_path) {
|
||||
Ok(rel) => StoragePath::from(rel.to_path_buf()),
|
||||
@@ -738,10 +612,9 @@ impl FolderRepository for FolderFsRepository {
|
||||
}
|
||||
};
|
||||
|
||||
// Get the folder entity with timeout
|
||||
let folder_result = timeout(
|
||||
Duration::from_secs(10),
|
||||
self.get_folder_by_storage_path(&rel_path)
|
||||
self.get_folder_by_path(&rel_path)
|
||||
).await;
|
||||
|
||||
match folder_result {
|
||||
@@ -761,68 +634,54 @@ impl FolderRepository for FolderFsRepository {
|
||||
current_idx += 1;
|
||||
}
|
||||
|
||||
// Save ID mappings
|
||||
if !folders.is_empty() {
|
||||
if let Err(e) = self.id_mapping_service.save_changes().await {
|
||||
tracing::error!("Error saving ID mappings: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Found {} folders in paginated request", folders.len());
|
||||
|
||||
Ok((folders, total_count))
|
||||
}
|
||||
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> FolderRepositoryResult<Folder> {
|
||||
// Get the original folder
|
||||
let original_folder = self.get_folder_by_id(id).await?;
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError> {
|
||||
let original_folder = self.get_folder(id).await?;
|
||||
tracing::debug!("Renaming folder with ID: {}, Name: {}", id, original_folder.name());
|
||||
|
||||
// Create an immutable new version of the folder with updated name
|
||||
let renamed_folder = original_folder.with_name(new_name)
|
||||
.map_err(|e| FolderRepositoryError::ValidationError(e.to_string()))?;
|
||||
.map_err(|e| DomainError::validation_error(e.to_string()))?;
|
||||
|
||||
// Check if target already exists
|
||||
if self.folder_exists_at_storage_path(renamed_folder.storage_path()).await? {
|
||||
return Err(FolderRepositoryError::AlreadyExists(renamed_folder.storage_path().to_string()));
|
||||
if self.check_folder_exists_at_storage_path(renamed_folder.storage_path()).await.map_err(DomainError::from)? {
|
||||
return Err(DomainError::already_exists("Folder", renamed_folder.storage_path().to_string()));
|
||||
}
|
||||
|
||||
// Rename the physical directory
|
||||
let abs_old_path = self.resolve_storage_path(original_folder.storage_path());
|
||||
let abs_new_path = self.resolve_storage_path(renamed_folder.storage_path());
|
||||
|
||||
fs::rename(&abs_old_path, &abs_new_path).await
|
||||
.map_err(FolderRepositoryError::IoError)?;
|
||||
.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?;
|
||||
|
||||
// Update the ID mapping
|
||||
self.id_mapping_service.update_path(id, renamed_folder.storage_path()).await
|
||||
.map_err(FolderRepositoryError::from)?;
|
||||
|
||||
// Save the updated mappings
|
||||
self.id_mapping_service.update_path(id, renamed_folder.storage_path()).await?;
|
||||
self.id_mapping_service.save_changes().await?;
|
||||
|
||||
tracing::debug!("Folder renamed successfully: ID={}, New name={}", id, renamed_folder.name());
|
||||
Ok(renamed_folder)
|
||||
}
|
||||
|
||||
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> FolderRepositoryResult<Folder> {
|
||||
// Get the original folder
|
||||
let original_folder = self.get_folder_by_id(id).await?;
|
||||
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> Result<Folder, DomainError> {
|
||||
let original_folder = self.get_folder(id).await?;
|
||||
tracing::debug!("Moving folder with ID: {}, Name: {}", id, original_folder.name());
|
||||
|
||||
// If the target parent is the same as current, no need to move
|
||||
if original_folder.parent_id() == new_parent_id {
|
||||
tracing::info!("Folder is already in the target parent, no need to move");
|
||||
return Ok(original_folder);
|
||||
}
|
||||
|
||||
// Get the target parent path
|
||||
let target_parent_storage_path = match new_parent_id {
|
||||
Some(parent_id) => {
|
||||
match self.get_folder_storage_path(parent_id).await {
|
||||
match self._get_folder_storage_path(parent_id).await {
|
||||
Ok(path) => Some(path),
|
||||
Err(e) => {
|
||||
return Err(FolderRepositoryError::Other(
|
||||
return Err(DomainError::internal_error("Folder",
|
||||
format!("Could not get target folder: {}", e)
|
||||
));
|
||||
}
|
||||
@@ -831,68 +690,50 @@ impl FolderRepository for FolderFsRepository {
|
||||
None => None
|
||||
};
|
||||
|
||||
// Create an immutable new version of the folder with updated parent
|
||||
let new_parent_id_option = new_parent_id.map(String::from);
|
||||
let moved_folder = original_folder.with_parent(new_parent_id_option, target_parent_storage_path)
|
||||
.map_err(|e| FolderRepositoryError::ValidationError(e.to_string()))?;
|
||||
.map_err(|e| DomainError::validation_error(e.to_string()))?;
|
||||
|
||||
// Check if target already exists
|
||||
if self.folder_exists_at_storage_path(moved_folder.storage_path()).await? {
|
||||
return Err(FolderRepositoryError::AlreadyExists(
|
||||
if self.check_folder_exists_at_storage_path(moved_folder.storage_path()).await.map_err(DomainError::from)? {
|
||||
return Err(DomainError::already_exists("Folder",
|
||||
format!("Folder already exists at destination: {}", moved_folder.storage_path().to_string())
|
||||
));
|
||||
}
|
||||
|
||||
// Move the physical directory
|
||||
let old_abs_path = self.resolve_storage_path(original_folder.storage_path());
|
||||
let new_abs_path = self.resolve_storage_path(moved_folder.storage_path());
|
||||
|
||||
// Ensure the target directory exists
|
||||
if let Some(parent) = new_abs_path.parent() {
|
||||
fs::create_dir_all(parent).await
|
||||
.map_err(FolderRepositoryError::IoError)?;
|
||||
.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?;
|
||||
}
|
||||
|
||||
// Move the directory physically (efficient rename operation)
|
||||
fs::rename(&old_abs_path, &new_abs_path).await
|
||||
.map_err(FolderRepositoryError::IoError)?;
|
||||
.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?;
|
||||
|
||||
tracing::info!("Folder moved successfully from {:?} to {:?}", old_abs_path, new_abs_path);
|
||||
|
||||
// Update the ID mapping
|
||||
self.id_mapping_service.update_path(id, moved_folder.storage_path()).await
|
||||
.map_err(FolderRepositoryError::from)?;
|
||||
|
||||
// Save the updated mappings
|
||||
self.id_mapping_service.update_path(id, moved_folder.storage_path()).await?;
|
||||
self.id_mapping_service.save_changes().await?;
|
||||
|
||||
tracing::debug!("Folder moved successfully: ID={}, New path={:?}", id, moved_folder.storage_path().to_string());
|
||||
Ok(moved_folder)
|
||||
}
|
||||
|
||||
async fn delete_folder(&self, id: &str) -> FolderRepositoryResult<()> {
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> {
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
// Get the folder first to check if it exists
|
||||
let folder = self.get_folder_by_id(id).await?;
|
||||
let folder = self.get_folder(id).await?;
|
||||
let folder_name = folder.name().to_string();
|
||||
let storage_path = folder.storage_path().clone();
|
||||
|
||||
tracing::info!("Deleting folder with ID: {}, Name: {}", id, folder_name);
|
||||
|
||||
// Para carpetas grandes, eliminar puede tomar tiempo
|
||||
// Lo manejamos en un task separado para no bloquear
|
||||
let abs_path = self.resolve_storage_path(&storage_path);
|
||||
|
||||
// Si la carpeta contiene muchos archivos, remove_dir_all puede tardar
|
||||
// usamos tokio::spawn para hacerlo en un task separado
|
||||
let path_for_display = abs_path.display().to_string();
|
||||
let path_for_deletion = abs_path.clone();
|
||||
|
||||
let delete_task = tokio::spawn(async move {
|
||||
tracing::debug!("Starting removal of folder: {}", path_for_display);
|
||||
|
||||
// Verificar si la carpeta existe y tiene muchas entradas
|
||||
let path_for_counting = path_for_deletion.clone();
|
||||
let entry_count = tokio::task::spawn_blocking(move || {
|
||||
let mut count = 0;
|
||||
@@ -900,19 +741,15 @@ impl FolderRepository for FolderFsRepository {
|
||||
for _ in entries {
|
||||
count += 1;
|
||||
if count > 1000 {
|
||||
break; // Solo necesitamos saber si es grande
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
}).await.unwrap_or(0);
|
||||
|
||||
// Para carpetas muy grandes, usar remove_dir_all puede causar bloqueos
|
||||
// Para carpetas pequeñas, usamos la versión asíncrona estándar
|
||||
if entry_count > 1000 {
|
||||
tracing::info!("Large folder detected with >1000 entries, using blocking removal");
|
||||
|
||||
// Para carpetas muy grandes, usamos spawn_blocking para no bloquear el runtime de tokio
|
||||
let path_for_large_removal = path_for_deletion.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Err(e) = std::fs::remove_dir_all(&path_for_large_removal) {
|
||||
@@ -930,13 +767,11 @@ impl FolderRepository for FolderFsRepository {
|
||||
))
|
||||
})
|
||||
} else {
|
||||
tracing::debug!("Using async removal for folder with {} entries", entry_count);
|
||||
fs::remove_dir_all(&path_for_deletion).await
|
||||
}
|
||||
});
|
||||
|
||||
// Esperar a que termine la eliminación con timeout
|
||||
const DELETE_TIMEOUT_SECS: u64 = 60; // 1 minuto máximo para eliminar
|
||||
const DELETE_TIMEOUT_SECS: u64 = 60;
|
||||
|
||||
let delete_result = timeout(
|
||||
Duration::from_secs(DELETE_TIMEOUT_SECS),
|
||||
@@ -948,29 +783,21 @@ impl FolderRepository for FolderFsRepository {
|
||||
match task_result {
|
||||
Ok(fs_result) => {
|
||||
if let Err(e) = fs_result {
|
||||
return Err(FolderRepositoryError::IoError(e));
|
||||
return Err(DomainError::internal_error("Folder", e.to_string()));
|
||||
}
|
||||
},
|
||||
Err(join_err) => {
|
||||
return Err(FolderRepositoryError::Other(
|
||||
return Err(DomainError::internal_error("Folder",
|
||||
format!("Task panicked during folder deletion: {}", join_err)
|
||||
));
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
// El timeout ocurrió, pero la tarea sigue ejecutándose en segundo plano
|
||||
tracing::warn!("Timeout waiting for folder deletion, continuing with ID removal");
|
||||
// No retornamos error, continuamos con la eliminación del ID
|
||||
}
|
||||
}
|
||||
|
||||
// Incluso si la eliminación física puede estar en progreso (timeout),
|
||||
// procedemos a eliminar la entrada del mapping
|
||||
// En el peor caso, si la eliminación física falla pero el ID se elimina,
|
||||
// la carpeta se quedará huérfana, pero no afectará al sistema
|
||||
|
||||
// Remove the ID mapping con timeout
|
||||
const MAPPING_TIMEOUT_SECS: u64 = 5;
|
||||
let remove_id_result = timeout(
|
||||
Duration::from_secs(MAPPING_TIMEOUT_SECS),
|
||||
@@ -978,46 +805,37 @@ impl FolderRepository for FolderFsRepository {
|
||||
).await;
|
||||
|
||||
match remove_id_result {
|
||||
Ok(result) => result.map_err(FolderRepositoryError::from)?,
|
||||
Ok(result) => result?,
|
||||
Err(_) => {
|
||||
return Err(FolderRepositoryError::Other(
|
||||
return Err(DomainError::internal_error("Folder",
|
||||
"Timeout removing folder ID from mapping".to_string()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Save the updated mappings (asíncrono, no esperamos)
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
tracing::info!("Folder deleted successfully: ID={}, Name={}", id, folder_name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult<bool> {
|
||||
self.check_folder_exists_at_storage_path(storage_path).await
|
||||
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
self.check_folder_exists_at_storage_path(storage_path).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_folder_storage_path(&self, id: &str) -> FolderRepositoryResult<StoragePath> {
|
||||
// Use the ID mapping service to get the storage path
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(id).await
|
||||
.map_err(FolderRepositoryError::from)?;
|
||||
|
||||
Ok(storage_path)
|
||||
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError> {
|
||||
self._get_folder_storage_path(id).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
// Legacy method implementations
|
||||
|
||||
async fn folder_exists(&self, path: &std::path::PathBuf) -> FolderRepositoryResult<bool> {
|
||||
let abs_path = self.resolve_legacy_path(path);
|
||||
Ok(abs_path.exists() && abs_path.is_dir())
|
||||
|
||||
async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError> {
|
||||
self._trash_move_to_trash(folder_id).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_folder_by_path(&self, path: &std::path::PathBuf) -> FolderRepositoryResult<Folder> {
|
||||
// Convert PathBuf to StoragePath
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
let storage_path = StoragePath::from_string(&path_str);
|
||||
|
||||
// Use the new method
|
||||
self.get_folder_by_storage_path(&storage_path).await
|
||||
|
||||
async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> Result<(), DomainError> {
|
||||
self._trash_restore_from_trash(folder_id, original_path).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError> {
|
||||
self._trash_delete_folder_permanently(folder_id).await.map_err(DomainError::from)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::domain::repositories::folder_repository::FolderRepositoryResult;
|
||||
use crate::infrastructure::repositories::repository_errors::FolderRepositoryResult;
|
||||
use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository;
|
||||
|
||||
// Este archivo contiene la implementación de los métodos relacionados con la papelera
|
||||
@@ -22,7 +22,7 @@ impl FolderFsRepository {
|
||||
// Asegurarse que el directorio de la papelera existe
|
||||
if !trash_dir.exists() {
|
||||
fs::create_dir_all(&trash_dir).await
|
||||
.map_err(|e| FolderRepositoryError::IoError(e))?;
|
||||
.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?;
|
||||
}
|
||||
|
||||
// Crear una ruta única para la carpeta en la papelera
|
||||
@@ -72,7 +72,7 @@ impl FolderFsRepository {
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error moviendo carpeta a papelera: {}", e);
|
||||
Err(FolderRepositoryError::IoError(e))
|
||||
Err(FolderRepositoryError::StorageError(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ impl FolderFsRepository {
|
||||
fs::create_dir_all(parent).await
|
||||
.map_err(|e| {
|
||||
error!("Error creando directorio padre para restauración: {}", e);
|
||||
FolderRepositoryError::IoError(e)
|
||||
FolderRepositoryError::StorageError(e.to_string())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ impl FolderFsRepository {
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error restaurando carpeta: {}", e);
|
||||
Err(FolderRepositoryError::IoError(e))
|
||||
Err(FolderRepositoryError::StorageError(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,7 +147,7 @@ impl FolderFsRepository {
|
||||
error!("Error eliminando carpeta permanentemente: {}", e);
|
||||
// No reportar error si la carpeta ya no existe
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
return Err(FolderRepositoryError::IoError(e));
|
||||
return Err(FolderRepositoryError::StorageError(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,4 +165,4 @@ impl FolderFsRepository {
|
||||
}
|
||||
|
||||
// Re-exportaciones necesarias para el compilador
|
||||
use crate::domain::repositories::folder_repository::FolderRepositoryError;
|
||||
use crate::infrastructure::repositories::repository_errors::FolderRepositoryError;
|
||||
@@ -1,14 +1,13 @@
|
||||
pub mod file_fs_repository;
|
||||
pub mod folder_fs_repository;
|
||||
pub mod parallel_file_processor;
|
||||
pub mod repository_errors;
|
||||
|
||||
// Nuevos repositorios refactorizados
|
||||
pub mod file_metadata_manager;
|
||||
pub mod file_path_resolver;
|
||||
// Repositorios CQRS (Read/Write) + composite
|
||||
pub mod file_fs_read_repository;
|
||||
pub mod file_fs_write_repository;
|
||||
pub mod composite_file_repository;
|
||||
|
||||
pub mod trash_fs_repository;
|
||||
pub mod file_fs_repository_trash;
|
||||
pub mod folder_fs_repository_trash;
|
||||
pub mod share_fs_repository;
|
||||
|
||||
@@ -16,8 +15,7 @@ pub mod share_fs_repository;
|
||||
pub mod pg;
|
||||
|
||||
// Re-exportar para facilitar acceso
|
||||
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 composite_file_repository::CompositeFileRepository;
|
||||
pub use pg::{UserPgRepository, SessionPgRepository};
|
||||
|
||||
@@ -10,7 +10,7 @@ use tracing::{info, debug, error};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::domain::repositories::file_repository::FileRepositoryError;
|
||||
use crate::infrastructure::repositories::repository_errors::FileRepositoryError;
|
||||
use crate::infrastructure::services::buffer_pool::BufferPool;
|
||||
|
||||
/// Structure for the byte range to process
|
||||
@@ -172,7 +172,7 @@ impl ParallelFileProcessor {
|
||||
pub async fn read_file_parallel(&self, file_path: &PathBuf) -> Result<Vec<u8>, FileRepositoryError> {
|
||||
// Get file size
|
||||
let metadata = tokio::fs::metadata(file_path).await
|
||||
.map_err(FileRepositoryError::IoError)?;
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
let file_size = metadata.len();
|
||||
|
||||
@@ -201,17 +201,17 @@ impl ParallelFileProcessor {
|
||||
if buffer.capacity() < file_size as usize {
|
||||
debug!("Buffer from pool too small ({}), using standard read", buffer.capacity());
|
||||
let content = tokio::fs::read(file_path).await
|
||||
.map_err(FileRepositoryError::IoError)?;
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
return Ok(content);
|
||||
}
|
||||
|
||||
// Use memory buffer from the pool
|
||||
let mut file = File::open(file_path).await
|
||||
.map_err(FileRepositoryError::IoError)?;
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
let read_size = file.read(buffer.as_mut_slice()).await
|
||||
.map_err(FileRepositoryError::IoError)?;
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
buffer.set_used(read_size);
|
||||
|
||||
@@ -221,7 +221,7 @@ impl ParallelFileProcessor {
|
||||
} else {
|
||||
// Standard implementation without pool
|
||||
let content = tokio::fs::read(file_path).await
|
||||
.map_err(FileRepositoryError::IoError)?;
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
return Ok(content);
|
||||
}
|
||||
@@ -241,7 +241,7 @@ impl ParallelFileProcessor {
|
||||
|
||||
// Open file once and share it
|
||||
let file = Arc::new(File::open(file_path).await
|
||||
.map_err(FileRepositoryError::IoError)?);
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?);
|
||||
|
||||
// Reference to BytesMut pool
|
||||
let bytes_pool = self.bytes_pool.clone();
|
||||
@@ -312,7 +312,7 @@ impl ParallelFileProcessor {
|
||||
Ok(Ok(())) => {},
|
||||
Ok(Err(e)) => {
|
||||
error!("Error in chunk {}: {}", i, e);
|
||||
return Err(FileRepositoryError::IoError(e));
|
||||
return Err(FileRepositoryError::StorageError(e.to_string()));
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Task error in chunk {}: {}", i, e);
|
||||
@@ -347,7 +347,7 @@ impl ParallelFileProcessor {
|
||||
|
||||
// Standard implementation (buffer pooling offers no advantages for simple writing)
|
||||
tokio::fs::write(file_path, content).await
|
||||
.map_err(FileRepositoryError::IoError)?;
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
@@ -358,7 +358,7 @@ impl ParallelFileProcessor {
|
||||
|
||||
// Create file (we don't use Mutex to reduce contention)
|
||||
let file = File::create(file_path).await
|
||||
.map_err(FileRepositoryError::IoError)?;
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
// Convert content to Bytes (single copy step)
|
||||
let content_bytes = Bytes::copy_from_slice(content);
|
||||
@@ -369,7 +369,7 @@ impl ParallelFileProcessor {
|
||||
// Process chunks in parallel
|
||||
for chunk in chunks {
|
||||
let file_clone = file.try_clone().await
|
||||
.map_err(FileRepositoryError::IoError)?;
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
let semaphore_clone = self.concurrency_limiter.clone();
|
||||
|
||||
// Create Bytes slice (doesn't copy data, only references)
|
||||
@@ -406,7 +406,7 @@ impl ParallelFileProcessor {
|
||||
Ok(Ok(())) => {},
|
||||
Ok(Err(e)) => {
|
||||
error!("Error in chunk {}: {}", i, e);
|
||||
return Err(FileRepositoryError::IoError(e));
|
||||
return Err(FileRepositoryError::StorageError(e.to_string()));
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Task error in chunk {}: {}", i, e);
|
||||
@@ -417,7 +417,7 @@ impl ParallelFileProcessor {
|
||||
|
||||
// Ensure everything has been written correctly
|
||||
let mut file_handle = file;
|
||||
file_handle.flush().await.map_err(FileRepositoryError::IoError)?;
|
||||
file_handle.flush().await.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
info!("Successfully wrote file of {}MB in parallel with optimized Bytes", file_size / (1024 * 1024));
|
||||
Ok(())
|
||||
@@ -427,6 +427,7 @@ impl ParallelFileProcessor {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytes::BufMut;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -27,28 +27,28 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
RETURNING id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(address_book.id)
|
||||
.bind(&address_book.name)
|
||||
.bind(&address_book.owner_id)
|
||||
.bind(&address_book.description)
|
||||
.bind(&address_book.color)
|
||||
.bind(address_book.is_public)
|
||||
.bind(address_book.created_at)
|
||||
.bind(address_book.updated_at)
|
||||
.bind(address_book.id())
|
||||
.bind(address_book.name())
|
||||
.bind(address_book.owner_id())
|
||||
.bind(address_book.description())
|
||||
.bind(address_book.color())
|
||||
.bind(address_book.is_public())
|
||||
.bind(address_book.created_at())
|
||||
.bind(address_book.updated_at())
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to create address book: {}", e)))?;
|
||||
|
||||
Ok(AddressBook {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
owner_id: row.get("owner_id"),
|
||||
description: row.get("description"),
|
||||
color: row.get("color"),
|
||||
is_public: row.get("is_public"),
|
||||
created_at: row.get("created_at"),
|
||||
updated_at: row.get("updated_at"),
|
||||
})
|
||||
Ok(AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
))
|
||||
}
|
||||
|
||||
async fn update_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook> {
|
||||
@@ -61,26 +61,26 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
RETURNING id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(&address_book.name)
|
||||
.bind(&address_book.description)
|
||||
.bind(&address_book.color)
|
||||
.bind(address_book.is_public)
|
||||
.bind(address_book.name())
|
||||
.bind(address_book.description())
|
||||
.bind(address_book.color())
|
||||
.bind(address_book.is_public())
|
||||
.bind(now)
|
||||
.bind(address_book.id)
|
||||
.bind(address_book.id())
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to update address book: {}", e)))?;
|
||||
|
||||
Ok(AddressBook {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
owner_id: row.get("owner_id"),
|
||||
description: row.get("description"),
|
||||
color: row.get("color"),
|
||||
is_public: row.get("is_public"),
|
||||
created_at: row.get("created_at"),
|
||||
updated_at: row.get("updated_at"),
|
||||
})
|
||||
Ok(AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
))
|
||||
}
|
||||
|
||||
async fn delete_address_book(&self, id: &Uuid) -> AddressBookRepositoryResult<()> {
|
||||
@@ -111,16 +111,16 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get address book by id: {}", e)))?;
|
||||
|
||||
let result = maybe_row.map(|row| AddressBook {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
owner_id: row.get("owner_id"),
|
||||
description: row.get("description"),
|
||||
color: row.get("color"),
|
||||
is_public: row.get("is_public"),
|
||||
created_at: row.get("created_at"),
|
||||
updated_at: row.get("updated_at"),
|
||||
});
|
||||
let result = maybe_row.map(|row| AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
));
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -140,16 +140,16 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get address books by owner: {}", e)))?;
|
||||
|
||||
let result = rows.into_iter()
|
||||
.map(|row| AddressBook {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
owner_id: row.get("owner_id"),
|
||||
description: row.get("description"),
|
||||
color: row.get("color"),
|
||||
is_public: row.get("is_public"),
|
||||
created_at: row.get("created_at"),
|
||||
updated_at: row.get("updated_at"),
|
||||
})
|
||||
.map(|row| AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
))
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
@@ -171,16 +171,16 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get shared address books: {}", e)))?;
|
||||
|
||||
let result = rows.into_iter()
|
||||
.map(|row| AddressBook {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
owner_id: row.get("owner_id"),
|
||||
description: row.get("description"),
|
||||
color: row.get("color"),
|
||||
is_public: row.get("is_public"),
|
||||
created_at: row.get("created_at"),
|
||||
updated_at: row.get("updated_at"),
|
||||
})
|
||||
.map(|row| AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
))
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
@@ -200,16 +200,16 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get public address books: {}", e)))?;
|
||||
|
||||
let result = rows.into_iter()
|
||||
.map(|row| AddressBook {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
owner_id: row.get("owner_id"),
|
||||
description: row.get("description"),
|
||||
color: row.get("color"),
|
||||
is_public: row.get("is_public"),
|
||||
created_at: row.get("created_at"),
|
||||
updated_at: row.get("updated_at"),
|
||||
})
|
||||
.map(|row| AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
))
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
|
||||
@@ -23,9 +23,9 @@ impl ContactPgRepository {
|
||||
impl ContactRepository for ContactPgRepository {
|
||||
async fn create_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact> {
|
||||
// Convert domain entities to persistence DTOs for JSONB serialization
|
||||
let email_dtos = emails_to_persistence(&contact.email);
|
||||
let phone_dtos = phones_to_persistence(&contact.phone);
|
||||
let address_dtos = addresses_to_persistence(&contact.address);
|
||||
let email_dtos = emails_to_persistence(contact.email());
|
||||
let phone_dtos = phones_to_persistence(contact.phone());
|
||||
let address_dtos = addresses_to_persistence(contact.address());
|
||||
|
||||
let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null);
|
||||
let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null);
|
||||
@@ -48,26 +48,26 @@ impl ContactRepository for ContactPgRepository {
|
||||
birthday, anniversary, vcard, etag, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(contact.id)
|
||||
.bind(contact.address_book_id)
|
||||
.bind(&contact.uid)
|
||||
.bind(&contact.full_name)
|
||||
.bind(&contact.first_name)
|
||||
.bind(&contact.last_name)
|
||||
.bind(&contact.nickname)
|
||||
.bind(contact.id())
|
||||
.bind(contact.address_book_id())
|
||||
.bind(contact.uid())
|
||||
.bind(contact.full_name_owned())
|
||||
.bind(contact.first_name_owned())
|
||||
.bind(contact.last_name_owned())
|
||||
.bind(contact.nickname_owned())
|
||||
.bind(email_json)
|
||||
.bind(phone_json)
|
||||
.bind(address_json)
|
||||
.bind(&contact.organization)
|
||||
.bind(&contact.title)
|
||||
.bind(&contact.notes)
|
||||
.bind(&contact.photo_url)
|
||||
.bind(contact.birthday)
|
||||
.bind(contact.anniversary)
|
||||
.bind(&contact.vcard)
|
||||
.bind(&contact.etag)
|
||||
.bind(contact.created_at)
|
||||
.bind(contact.updated_at)
|
||||
.bind(contact.organization_owned())
|
||||
.bind(contact.title_owned())
|
||||
.bind(contact.notes_owned())
|
||||
.bind(contact.photo_url_owned())
|
||||
.bind(contact.birthday().copied())
|
||||
.bind(contact.anniversary().copied())
|
||||
.bind(contact.vcard())
|
||||
.bind(contact.etag())
|
||||
.bind(*contact.created_at())
|
||||
.bind(*contact.updated_at())
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to create contact: {}", e)))?;
|
||||
@@ -80,9 +80,9 @@ impl ContactRepository for ContactPgRepository {
|
||||
async fn update_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact> {
|
||||
let now = Utc::now();
|
||||
// Convert domain entities to persistence DTOs for JSONB serialization
|
||||
let email_dtos = emails_to_persistence(&contact.email);
|
||||
let phone_dtos = phones_to_persistence(&contact.phone);
|
||||
let address_dtos = addresses_to_persistence(&contact.address);
|
||||
let email_dtos = emails_to_persistence(contact.email());
|
||||
let phone_dtos = phones_to_persistence(contact.phone());
|
||||
let address_dtos = addresses_to_persistence(contact.address());
|
||||
|
||||
let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null);
|
||||
let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null);
|
||||
@@ -90,7 +90,7 @@ impl ContactRepository for ContactPgRepository {
|
||||
|
||||
// Create a clone of the contact with the updated timestamp
|
||||
let mut updated_contact = contact.clone();
|
||||
updated_contact.updated_at = now;
|
||||
updated_contact.set_updated_at(now);
|
||||
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
@@ -119,23 +119,23 @@ impl ContactRepository for ContactPgRepository {
|
||||
birthday, anniversary, vcard, etag, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(&updated_contact.full_name)
|
||||
.bind(&updated_contact.first_name)
|
||||
.bind(&updated_contact.last_name)
|
||||
.bind(&updated_contact.nickname)
|
||||
.bind(updated_contact.full_name_owned())
|
||||
.bind(updated_contact.first_name_owned())
|
||||
.bind(updated_contact.last_name_owned())
|
||||
.bind(updated_contact.nickname_owned())
|
||||
.bind(email_json)
|
||||
.bind(phone_json)
|
||||
.bind(address_json)
|
||||
.bind(&updated_contact.organization)
|
||||
.bind(&updated_contact.title)
|
||||
.bind(&updated_contact.notes)
|
||||
.bind(&updated_contact.photo_url)
|
||||
.bind(updated_contact.birthday)
|
||||
.bind(updated_contact.anniversary)
|
||||
.bind(&updated_contact.vcard)
|
||||
.bind(&updated_contact.etag)
|
||||
.bind(updated_contact.organization_owned())
|
||||
.bind(updated_contact.title_owned())
|
||||
.bind(updated_contact.notes_owned())
|
||||
.bind(updated_contact.photo_url_owned())
|
||||
.bind(updated_contact.birthday().copied())
|
||||
.bind(updated_contact.anniversary().copied())
|
||||
.bind(updated_contact.vcard())
|
||||
.bind(updated_contact.etag())
|
||||
.bind(now)
|
||||
.bind(updated_contact.id)
|
||||
.bind(updated_contact.id())
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to update contact: {}", e)))?;
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{PgPool, Row};
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::favorites_dto::FavoriteItemDto;
|
||||
use crate::application::ports::favorites_ports::FavoritesRepositoryPort;
|
||||
use crate::common::errors::{Result, DomainError, ErrorKind};
|
||||
|
||||
/// Implementación PostgreSQL del puerto de persistencia de favoritos.
|
||||
pub struct FavoritesPgRepository {
|
||||
db_pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl FavoritesPgRepository {
|
||||
pub fn new(db_pool: Arc<PgPool>) -> Self {
|
||||
Self { db_pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FavoritesRepositoryPort for FavoritesPgRepository {
|
||||
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>> {
|
||||
let user_uuid = Uuid::parse_str(user_id)?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id::TEXT AS "id",
|
||||
user_id::TEXT AS "user_id",
|
||||
item_id AS "item_id",
|
||||
item_type AS "item_type",
|
||||
created_at AS "created_at"
|
||||
FROM auth.user_favorites
|
||||
WHERE user_id = $1::TEXT
|
||||
ORDER BY created_at DESC
|
||||
"#,
|
||||
)
|
||||
.bind(user_uuid)
|
||||
.fetch_all(&*self.db_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error fetching favorites: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to fetch favorites: {}", e))
|
||||
})?;
|
||||
|
||||
let favorites = rows
|
||||
.iter()
|
||||
.map(|row| FavoriteItemDto {
|
||||
id: row.get("id"),
|
||||
user_id: row.get("user_id"),
|
||||
item_id: row.get("item_id"),
|
||||
item_type: row.get("item_type"),
|
||||
created_at: row.get("created_at"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(favorites)
|
||||
}
|
||||
|
||||
async fn add_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
|
||||
let user_uuid = Uuid::parse_str(user_id)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO auth.user_favorites (user_id, item_id, item_type)
|
||||
VALUES ($1::TEXT, $2, $3)
|
||||
ON CONFLICT (user_id, item_id, item_type) DO NOTHING
|
||||
"#,
|
||||
)
|
||||
.bind(user_uuid)
|
||||
.bind(item_id)
|
||||
.bind(item_type)
|
||||
.execute(&*self.db_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error adding favorite: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to add to favorites: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
|
||||
let user_uuid = Uuid::parse_str(user_id)?;
|
||||
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM auth.user_favorites
|
||||
WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3
|
||||
"#,
|
||||
)
|
||||
.bind(user_uuid)
|
||||
.bind(item_id)
|
||||
.bind(item_type)
|
||||
.execute(&*self.db_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error removing favorite: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to remove from favorites: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
|
||||
let user_uuid = Uuid::parse_str(user_id)?;
|
||||
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM auth.user_favorites
|
||||
WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3
|
||||
) AS "is_favorite"
|
||||
"#,
|
||||
)
|
||||
.bind(user_uuid)
|
||||
.bind(item_id)
|
||||
.bind(item_type)
|
||||
.fetch_one(&*self.db_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error checking favorite status: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to check favorite status: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(row.try_get("is_favorite").unwrap_or(false))
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ mod calendar_pg_repository;
|
||||
mod calendar_event_pg_repository;
|
||||
mod contact_pg_repository;
|
||||
mod contact_persistence_dto;
|
||||
mod favorites_pg_repository;
|
||||
mod recent_items_pg_repository;
|
||||
mod session_pg_repository;
|
||||
mod transaction_utils;
|
||||
mod user_pg_repository;
|
||||
@@ -12,5 +14,7 @@ pub use calendar_pg_repository::CalendarPgRepository;
|
||||
pub use calendar_event_pg_repository::CalendarEventPgRepository;
|
||||
pub use contact_pg_repository::ContactPgRepository;
|
||||
pub use contact_persistence_dto::*;
|
||||
pub use favorites_pg_repository::FavoritesPgRepository;
|
||||
pub use recent_items_pg_repository::RecentItemsPgRepository;
|
||||
pub use session_pg_repository::SessionPgRepository;
|
||||
pub use user_pg_repository::UserPgRepository;
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{PgPool, Row};
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::recent_dto::RecentItemDto;
|
||||
use crate::application::ports::recent_ports::RecentItemsRepositoryPort;
|
||||
use crate::common::errors::{Result, DomainError, ErrorKind};
|
||||
|
||||
/// Implementación PostgreSQL del puerto de persistencia de elementos recientes.
|
||||
pub struct RecentItemsPgRepository {
|
||||
db_pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl RecentItemsPgRepository {
|
||||
pub fn new(db_pool: Arc<PgPool>) -> Self {
|
||||
Self { db_pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
||||
async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result<Vec<RecentItemDto>> {
|
||||
let user_uuid = Uuid::parse_str(user_id)?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id::TEXT AS "id",
|
||||
user_id::TEXT AS "user_id",
|
||||
item_id AS "item_id",
|
||||
item_type AS "item_type",
|
||||
accessed_at AS "accessed_at"
|
||||
FROM auth.user_recent_files
|
||||
WHERE user_id = $1::TEXT
|
||||
ORDER BY accessed_at DESC
|
||||
LIMIT $2
|
||||
"#,
|
||||
)
|
||||
.bind(user_uuid)
|
||||
.bind(limit)
|
||||
.fetch_all(&*self.db_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error fetching recent items: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to fetch recent items: {}", e))
|
||||
})?;
|
||||
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(|row| RecentItemDto {
|
||||
id: row.get("id"),
|
||||
user_id: row.get("user_id"),
|
||||
item_id: row.get("item_id"),
|
||||
item_type: row.get("item_type"),
|
||||
accessed_at: row.get("accessed_at"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
async fn upsert_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
|
||||
let user_uuid = Uuid::parse_str(user_id)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO auth.user_recent_files (user_id, item_id, item_type, accessed_at)
|
||||
VALUES ($1::TEXT, $2, $3, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (user_id, item_id, item_type)
|
||||
DO UPDATE SET accessed_at = CURRENT_TIMESTAMP
|
||||
"#,
|
||||
)
|
||||
.bind(user_uuid)
|
||||
.bind(item_id)
|
||||
.bind(item_type)
|
||||
.execute(&*self.db_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error upserting recent item access: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to record item access: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
|
||||
let user_uuid = Uuid::parse_str(user_id)?;
|
||||
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM auth.user_recent_files
|
||||
WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3
|
||||
"#,
|
||||
)
|
||||
.bind(user_uuid)
|
||||
.bind(item_id)
|
||||
.bind(item_type)
|
||||
.execute(&*self.db_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error removing recent item: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to remove recent item: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
async fn clear_all(&self, user_id: &str) -> Result<()> {
|
||||
let user_uuid = Uuid::parse_str(user_id)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM auth.user_recent_files
|
||||
WHERE user_id = $1::TEXT
|
||||
"#,
|
||||
)
|
||||
.bind(user_uuid)
|
||||
.execute(&*self.db_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error clearing recent items: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to clear recent items: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prune(&self, user_id: &str, max_items: i32) -> Result<()> {
|
||||
let user_uuid = Uuid::parse_str(user_id)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM auth.user_recent_files
|
||||
WHERE id IN (
|
||||
SELECT id FROM auth.user_recent_files
|
||||
WHERE user_id = $1::TEXT
|
||||
ORDER BY accessed_at DESC
|
||||
OFFSET $2
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(user_uuid)
|
||||
.bind(max_items)
|
||||
.execute(&*self.db_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error pruning old recent items: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to prune recent items: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -66,8 +66,8 @@ impl SessionRepository for SessionPgRepository {
|
||||
.bind(session_clone.user_id())
|
||||
.bind(session_clone.refresh_token())
|
||||
.bind(session_clone.expires_at())
|
||||
.bind(&session_clone.ip_address)
|
||||
.bind(&session_clone.user_agent)
|
||||
.bind(session_clone.ip_address())
|
||||
.bind(session_clone.user_agent())
|
||||
.bind(session_clone.created_at())
|
||||
.bind(session_clone.is_revoked())
|
||||
.execute(&mut **tx)
|
||||
@@ -120,16 +120,16 @@ impl SessionRepository for SessionPgRepository {
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
Ok(Session {
|
||||
id: row.get("id"),
|
||||
user_id: row.get("user_id"),
|
||||
refresh_token: row.get("refresh_token"),
|
||||
expires_at: row.get("expires_at"),
|
||||
ip_address: row.get("ip_address"),
|
||||
user_agent: row.get("user_agent"),
|
||||
created_at: row.get("created_at"),
|
||||
revoked: row.get("revoked"),
|
||||
})
|
||||
Ok(Session::from_raw(
|
||||
row.get("id"),
|
||||
row.get("user_id"),
|
||||
row.get("refresh_token"),
|
||||
row.get("expires_at"),
|
||||
row.get("ip_address"),
|
||||
row.get("user_agent"),
|
||||
row.get("created_at"),
|
||||
row.get("revoked"),
|
||||
))
|
||||
}
|
||||
|
||||
/// Obtiene una sesión por token de actualización
|
||||
@@ -148,16 +148,16 @@ impl SessionRepository for SessionPgRepository {
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
Ok(Session {
|
||||
id: row.get("id"),
|
||||
user_id: row.get("user_id"),
|
||||
refresh_token: row.get("refresh_token"),
|
||||
expires_at: row.get("expires_at"),
|
||||
ip_address: row.get("ip_address"),
|
||||
user_agent: row.get("user_agent"),
|
||||
created_at: row.get("created_at"),
|
||||
revoked: row.get("revoked"),
|
||||
})
|
||||
Ok(Session::from_raw(
|
||||
row.get("id"),
|
||||
row.get("user_id"),
|
||||
row.get("refresh_token"),
|
||||
row.get("expires_at"),
|
||||
row.get("ip_address"),
|
||||
row.get("user_agent"),
|
||||
row.get("created_at"),
|
||||
row.get("revoked"),
|
||||
))
|
||||
}
|
||||
|
||||
/// Obtiene todas las sesiones de un usuario
|
||||
@@ -179,16 +179,16 @@ impl SessionRepository for SessionPgRepository {
|
||||
|
||||
let sessions = rows.into_iter()
|
||||
.map(|row| {
|
||||
Session {
|
||||
id: row.get("id"),
|
||||
user_id: row.get("user_id"),
|
||||
refresh_token: row.get("refresh_token"),
|
||||
expires_at: row.get("expires_at"),
|
||||
ip_address: row.get("ip_address"),
|
||||
user_agent: row.get("user_agent"),
|
||||
created_at: row.get("created_at"),
|
||||
revoked: row.get("revoked"),
|
||||
}
|
||||
Session::from_raw(
|
||||
row.get("id"),
|
||||
row.get("user_id"),
|
||||
row.get("refresh_token"),
|
||||
row.get("expires_at"),
|
||||
row.get("ip_address"),
|
||||
row.get("user_agent"),
|
||||
row.get("created_at"),
|
||||
row.get("revoked"),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
//! Infrastructure-layer error types for file and folder repository operations.
|
||||
//!
|
||||
//! These error types are used internally by the filesystem repository implementations
|
||||
//! (FileFsReadRepository, FileFsWriteRepository, FolderFsRepository, etc.) to represent
|
||||
//! errors that can occur during storage operations. They are converted to `DomainError`
|
||||
//! at the port boundary before crossing into the application layer.
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Error types for file repository operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FileRepositoryError {
|
||||
#[error("File not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("File already exists: {0}")]
|
||||
AlreadyExists(String),
|
||||
|
||||
#[error("Invalid file path: {0}")]
|
||||
InvalidPath(String),
|
||||
|
||||
#[error("Operation not supported: {0}")]
|
||||
OperationNotSupported(String),
|
||||
|
||||
#[error("Storage error: {0}")]
|
||||
StorageError(String),
|
||||
|
||||
#[error("Domain error: {0}")]
|
||||
DomainError(#[from] DomainError),
|
||||
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type FileRepositoryResult<T> = Result<T, FileRepositoryError>;
|
||||
|
||||
/// Error types for folder repository operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FolderRepositoryError {
|
||||
#[error("Folder not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Folder already exists: {0}")]
|
||||
AlreadyExists(String),
|
||||
|
||||
#[error("Invalid folder path: {0}")]
|
||||
InvalidPath(String),
|
||||
|
||||
#[error("Operation not supported: {0}")]
|
||||
OperationNotSupported(String),
|
||||
|
||||
#[error("Storage error: {0}")]
|
||||
StorageError(String),
|
||||
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(String),
|
||||
|
||||
#[error("Domain error: {0}")]
|
||||
DomainError(#[from] DomainError),
|
||||
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type FolderRepositoryResult<T> = Result<T, FolderRepositoryError>;
|
||||
|
||||
// ── Conversions to DomainError ──
|
||||
|
||||
impl From<FileRepositoryError> for DomainError {
|
||||
fn from(err: FileRepositoryError) -> Self {
|
||||
match err {
|
||||
FileRepositoryError::NotFound(id) => DomainError::not_found("File", id),
|
||||
FileRepositoryError::AlreadyExists(path) => DomainError::already_exists("File", path),
|
||||
FileRepositoryError::InvalidPath(path) => DomainError::validation_error(format!("Invalid path: {}", path)),
|
||||
FileRepositoryError::StorageError(msg) => DomainError::internal_error("File", format!("Storage error: {}", msg)),
|
||||
FileRepositoryError::Other(msg) => DomainError::internal_error("File", msg),
|
||||
FileRepositoryError::OperationNotSupported(msg) => DomainError::operation_not_supported("File", msg),
|
||||
FileRepositoryError::DomainError(e) => e,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FolderRepositoryError> for DomainError {
|
||||
fn from(err: FolderRepositoryError) -> Self {
|
||||
match err {
|
||||
FolderRepositoryError::NotFound(id) => DomainError::not_found("Folder", id),
|
||||
FolderRepositoryError::AlreadyExists(path) => DomainError::already_exists("Folder", path),
|
||||
FolderRepositoryError::InvalidPath(path) => DomainError::validation_error(format!("Invalid path: {}", path)),
|
||||
FolderRepositoryError::StorageError(msg) => DomainError::internal_error("Folder", format!("Storage error: {}", msg)),
|
||||
FolderRepositoryError::ValidationError(msg) => DomainError::validation_error(msg),
|
||||
FolderRepositoryError::Other(msg) => DomainError::internal_error("Folder", msg),
|
||||
FolderRepositoryError::OperationNotSupported(msg) => DomainError::operation_not_supported("Folder", msg),
|
||||
FolderRepositoryError::DomainError(e) => e,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,35 +83,35 @@ impl ShareFsRepository {
|
||||
record.permissions_reshare,
|
||||
);
|
||||
|
||||
Share {
|
||||
id: record.id.clone(),
|
||||
item_id: record.item_id.clone(),
|
||||
Share::from_raw(
|
||||
record.id.clone(),
|
||||
record.item_id.clone(),
|
||||
item_type,
|
||||
token: record.token.clone(),
|
||||
password_hash: record.password_hash.clone(),
|
||||
expires_at: record.expires_at,
|
||||
record.token.clone(),
|
||||
record.password_hash.clone(),
|
||||
record.expires_at,
|
||||
permissions,
|
||||
created_at: record.created_at,
|
||||
created_by: record.created_by.clone(),
|
||||
access_count: record.access_count,
|
||||
}
|
||||
record.created_at,
|
||||
record.created_by.clone(),
|
||||
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,
|
||||
id: share.id().to_string(),
|
||||
item_id: share.item_id().to_string(),
|
||||
item_type: share.item_type().to_string(),
|
||||
token: share.token().to_string(),
|
||||
password_hash: share.password_hash().map(|s| s.to_string()),
|
||||
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().to_string(),
|
||||
access_count: share.access_count(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ impl ShareStoragePort for ShareFsRepository {
|
||||
.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 existing_index = shares.iter().position(|s| s.id == share.id());
|
||||
|
||||
let record = self.to_record(share);
|
||||
|
||||
@@ -191,9 +191,9 @@ impl ShareStoragePort for ShareFsRepository {
|
||||
.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)
|
||||
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))
|
||||
DomainError::not_found("Share", format!("Share with ID {} not found for update", share.id()))
|
||||
})?;
|
||||
|
||||
// Actualiza el registro
|
||||
|
||||
@@ -194,32 +194,32 @@ impl TrashFsRepository {
|
||||
))?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
Ok(TrashedItem {
|
||||
Ok(TrashedItem::from_raw(
|
||||
id,
|
||||
original_id,
|
||||
user_id,
|
||||
item_type,
|
||||
name: entry.name,
|
||||
original_path: entry.original_path,
|
||||
entry.name,
|
||||
entry.original_path,
|
||||
trashed_at,
|
||||
deletion_date,
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
/// Convierte una entidad TrashedItem a entrada JSON
|
||||
fn trashed_item_to_entry(&self, item: &TrashedItem) -> TrashedItemEntry {
|
||||
TrashedItemEntry {
|
||||
id: item.id.to_string(),
|
||||
original_id: item.original_id.to_string(),
|
||||
user_id: item.user_id.to_string(),
|
||||
item_type: match item.item_type {
|
||||
id: item.id().to_string(),
|
||||
original_id: item.original_id().to_string(),
|
||||
user_id: item.user_id().to_string(),
|
||||
item_type: match item.item_type() {
|
||||
TrashedItemType::File => "file".to_string(),
|
||||
TrashedItemType::Folder => "folder".to_string(),
|
||||
},
|
||||
name: item.name.clone(),
|
||||
original_path: item.original_path.clone(),
|
||||
trashed_at: item.trashed_at.to_rfc3339(),
|
||||
deletion_date: item.deletion_date.to_rfc3339(),
|
||||
name: item.name().to_string(),
|
||||
original_path: item.original_path().to_string(),
|
||||
trashed_at: item.trashed_at().to_rfc3339(),
|
||||
deletion_date: item.deletion_date().to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,10 +228,10 @@ impl TrashFsRepository {
|
||||
impl TrashRepository for TrashFsRepository {
|
||||
#[instrument(skip(self))]
|
||||
async fn add_to_trash(&self, item: &TrashedItem) -> Result<()> {
|
||||
debug!("Añadiendo elemento a la papelera: id={}, user={}", item.id, item.user_id);
|
||||
debug!("Añadiendo elemento a la papelera: id={}, user={}", item.id(), item.user_id());
|
||||
|
||||
// Aseguramos que existe el directorio de la papelera para este usuario
|
||||
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());
|
||||
|
||||
// Create the user-specific trash directory
|
||||
|
||||
@@ -89,9 +89,11 @@ impl BufferPool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Obtiene un buffer del pool o crea uno nuevo si es necesario
|
||||
/// Obtiene un buffer del pool o crea uno nuevo si es necesario.
|
||||
/// This version takes an Arc<Self> to ensure the BorrowedBuffer keeps a proper
|
||||
/// reference to the shared pool (not a clone).
|
||||
#[allow(unused_variables)]
|
||||
pub async fn get_buffer(&self) -> BorrowedBuffer {
|
||||
pub async fn get_buffer(self: &Arc<Self>) -> BorrowedBuffer {
|
||||
// Incrementar contador de gets
|
||||
{
|
||||
let mut stats = self.stats.lock().await;
|
||||
@@ -99,27 +101,31 @@ impl BufferPool {
|
||||
}
|
||||
|
||||
// Control de concurrencia
|
||||
// Usando el mecanismo RAII de Rust para gestión automática
|
||||
// de recursos al finalizar la función
|
||||
let _ = match self.limit.try_acquire() {
|
||||
Ok(_permit) => _permit, // _ prefix para indicar que es intencional
|
||||
// Acquire a semaphore permit. If none available, wait.
|
||||
// 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).
|
||||
match self.limit.try_acquire() {
|
||||
Ok(permit) => permit.forget(),
|
||||
Err(_) => {
|
||||
// No hay permisos disponibles, esperamos
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.waits += 1;
|
||||
stats.max_buffers_reached += 1;
|
||||
drop(stats);
|
||||
{
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.waits += 1;
|
||||
stats.max_buffers_reached += 1;
|
||||
}
|
||||
|
||||
debug!("Buffer pool: waiting for available buffer");
|
||||
let _permit = self.limit.acquire().await.expect("Semaphore should not be closed");
|
||||
let permit = self.limit.acquire().await.expect("Semaphore should not be closed");
|
||||
debug!("Buffer pool: acquired buffer after waiting");
|
||||
_permit
|
||||
permit.forget();
|
||||
}
|
||||
};
|
||||
|
||||
// Intentar obtener un buffer existente del pool
|
||||
let mut pool_locked = self.pool.lock().await;
|
||||
|
||||
let pool_arc = Arc::clone(self);
|
||||
|
||||
if let Some(mut pooled_buffer) = pool_locked.pop_front() {
|
||||
// Verificar si el buffer ha expirado
|
||||
if pooled_buffer.last_used.elapsed() > self.buffer_ttl {
|
||||
@@ -137,7 +143,7 @@ impl BufferPool {
|
||||
BorrowedBuffer {
|
||||
buffer: vec![0; self.buffer_size],
|
||||
used_size: 0,
|
||||
pool: Arc::new(self.clone()),
|
||||
pool: pool_arc,
|
||||
return_to_pool: true,
|
||||
}
|
||||
} else {
|
||||
@@ -155,7 +161,7 @@ impl BufferPool {
|
||||
BorrowedBuffer {
|
||||
buffer: pooled_buffer.buffer,
|
||||
used_size: 0,
|
||||
pool: Arc::new(self.clone()),
|
||||
pool: pool_arc,
|
||||
return_to_pool: true,
|
||||
}
|
||||
}
|
||||
@@ -173,7 +179,7 @@ impl BufferPool {
|
||||
BorrowedBuffer {
|
||||
buffer: vec![0; self.buffer_size],
|
||||
used_size: 0,
|
||||
pool: Arc::new(self.clone()),
|
||||
pool: pool_arc,
|
||||
return_to_pool: true,
|
||||
}
|
||||
}
|
||||
@@ -185,6 +191,8 @@ impl BufferPool {
|
||||
if buffer.capacity() != self.buffer_size {
|
||||
debug!("Buffer pool: discarding buffer of wrong size: {} (expected {})",
|
||||
buffer.capacity(), self.buffer_size);
|
||||
// Release the semaphore permit even if we discard the buffer
|
||||
self.limit.add_permits(1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -202,6 +210,11 @@ impl BufferPool {
|
||||
// Actualizar estadísticas
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.returns += 1;
|
||||
|
||||
// Release the semaphore permit so another caller can acquire a buffer
|
||||
drop(pool_locked);
|
||||
drop(stats);
|
||||
self.limit.add_permits(1);
|
||||
}
|
||||
|
||||
/// Limpia buffers expirados del pool
|
||||
@@ -331,9 +344,13 @@ impl Drop for BorrowedBuffer {
|
||||
let pool = self.pool.clone();
|
||||
|
||||
// Spawn del return para que el drop no bloquee
|
||||
// return_buffer will release the semaphore permit
|
||||
tokio::spawn(async move {
|
||||
pool.return_buffer(buffer).await;
|
||||
});
|
||||
} else {
|
||||
// Buffer not returned to pool, but we still need to release the semaphore permit
|
||||
self.pool.limit.add_permits(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time;
|
||||
use futures::future::BoxFuture;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Representación de metadatos en caché
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedMetadata {
|
||||
/// Si el archivo o directorio existe
|
||||
pub exists: bool,
|
||||
/// Tamaño en bytes (para archivos)
|
||||
pub size: Option<u64>,
|
||||
/// Timestamp de creación
|
||||
pub created_at: Option<u64>,
|
||||
/// Timestamp de modificación
|
||||
pub modified_at: Option<u64>,
|
||||
/// Tiempo de expiración de la caché
|
||||
expires_at: Instant,
|
||||
}
|
||||
|
||||
/// Estructura para gestionar la caché de metadatos de archivos y directorios
|
||||
pub struct StorageCacheManager {
|
||||
/// Caché de existencia y metadatos
|
||||
cache: RwLock<HashMap<PathBuf, CachedMetadata>>,
|
||||
/// TTL para entradas de archivos (milisegundos)
|
||||
file_ttl_ms: u64,
|
||||
/// TTL para entradas de directorios (milisegundos)
|
||||
dir_ttl_ms: u64,
|
||||
/// Tamaño máximo de caché
|
||||
max_entries: usize,
|
||||
}
|
||||
|
||||
impl StorageCacheManager {
|
||||
/// Crea una nueva instancia del gestor de caché
|
||||
pub fn new(file_ttl_ms: u64, dir_ttl_ms: u64, max_entries: usize) -> Self {
|
||||
Self {
|
||||
cache: RwLock::new(HashMap::with_capacity(max_entries)),
|
||||
file_ttl_ms,
|
||||
dir_ttl_ms,
|
||||
max_entries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea una instancia por defecto del gestor de caché
|
||||
pub fn default() -> Self {
|
||||
Self::new(
|
||||
60_000, // 1 minuto para archivos
|
||||
300_000, // 5 minutos para directorios
|
||||
10_000, // máximo 10,000 entradas
|
||||
)
|
||||
}
|
||||
|
||||
/// Verifica si un archivo o directorio existe en caché
|
||||
pub async fn check_exists(&self, path: &PathBuf, _is_dir: bool) -> Result<bool, ()> {
|
||||
// Intentar obtener de la caché
|
||||
if let Some(metadata) = self.get_cached_metadata(path).await {
|
||||
return Ok(metadata.exists);
|
||||
}
|
||||
|
||||
// No está en caché
|
||||
Err(())
|
||||
}
|
||||
|
||||
/// Obtiene los metadatos de un path desde la caché
|
||||
async fn get_cached_metadata(&self, path: &PathBuf) -> Option<CachedMetadata> {
|
||||
let cache = self.cache.read().await;
|
||||
|
||||
if let Some(metadata) = cache.get(path) {
|
||||
// Verificar si la entrada expiró
|
||||
if Instant::now() < metadata.expires_at {
|
||||
return Some(metadata.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Actualiza la caché con los metadatos de un path
|
||||
pub async fn update_cache(&self, path: &PathBuf, exists: bool, size: Option<u64>,
|
||||
created_at: Option<u64>, modified_at: Option<u64>, is_dir: bool) {
|
||||
let mut cache = self.cache.write().await;
|
||||
|
||||
// Si la caché está llena, eliminar entradas aleatorias antes de agregar
|
||||
if cache.len() >= self.max_entries {
|
||||
self.evict_entries(&mut cache, 100).await;
|
||||
}
|
||||
|
||||
// Determinar TTL basado en si es archivo o directorio
|
||||
let ttl = if is_dir {
|
||||
Duration::from_millis(self.dir_ttl_ms)
|
||||
} else {
|
||||
Duration::from_millis(self.file_ttl_ms)
|
||||
};
|
||||
|
||||
// Crear metadatos y agregar a la caché
|
||||
let metadata = CachedMetadata {
|
||||
exists,
|
||||
size,
|
||||
created_at,
|
||||
modified_at,
|
||||
expires_at: Instant::now() + ttl,
|
||||
};
|
||||
|
||||
cache.insert(path.clone(), metadata);
|
||||
}
|
||||
|
||||
/// Elimina entradas aleatorias de la caché cuando está llena
|
||||
async fn evict_entries(&self, cache: &mut HashMap<PathBuf, CachedMetadata>, count: usize) {
|
||||
// Obtener las entradas más antiguas para eliminar
|
||||
let mut entries: Vec<_> = cache.keys().cloned().collect();
|
||||
|
||||
// Limitar el número de entradas a eliminar
|
||||
let to_remove = count.min(entries.len() / 10);
|
||||
|
||||
if to_remove == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Eliminar las primeras entradas (implementación simple)
|
||||
entries.truncate(to_remove);
|
||||
|
||||
for path in entries {
|
||||
cache.remove(&path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Inicia una tarea de limpieza periódica
|
||||
pub fn start_cleanup_task(cache_manager: Arc<Self>) -> BoxFuture<'static, ()> {
|
||||
Box::pin(async move {
|
||||
let interval = Duration::from_secs(60); // Ejecutar cada minuto
|
||||
|
||||
loop {
|
||||
time::sleep(interval).await;
|
||||
|
||||
// Limpiar entradas expiradas
|
||||
let now = Instant::now();
|
||||
let mut cache = cache_manager.cache.write().await;
|
||||
|
||||
// Encontrar entradas expiradas
|
||||
let expired: Vec<_> = cache
|
||||
.iter()
|
||||
.filter(|(_, metadata)| now > metadata.expires_at)
|
||||
.map(|(path, _)| path.clone())
|
||||
.collect();
|
||||
|
||||
// Eliminar entradas expiradas
|
||||
for path in expired {
|
||||
cache.remove(&path);
|
||||
}
|
||||
|
||||
// Registrar estadísticas
|
||||
let cache_size = cache.len();
|
||||
drop(cache);
|
||||
|
||||
tracing::debug!("Cache cleanup completed. Entries remaining: {}", cache_size);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Invalida una entrada específica de la caché
|
||||
pub async fn invalidate(&self, path: &PathBuf) {
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.remove(path);
|
||||
}
|
||||
|
||||
/// Invalida todas las entradas de la caché relacionadas con una carpeta
|
||||
pub async fn invalidate_folder(&self, folder_path: &PathBuf) {
|
||||
let mut cache = self.cache.write().await;
|
||||
|
||||
// Eliminar entradas que sean descendientes de la carpeta
|
||||
let folder_str = folder_path.to_string_lossy().to_string();
|
||||
|
||||
// Encontrar entradas a eliminar
|
||||
let to_remove: Vec<_> = cache
|
||||
.keys()
|
||||
.filter_map(|path| {
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
if path_str.starts_with(&folder_str) {
|
||||
Some(path.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Eliminar las entradas
|
||||
for path in to_remove {
|
||||
cache.remove(&path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtiene el número actual de entradas en la caché
|
||||
pub async fn cache_size(&self) -> usize {
|
||||
let cache = self.cache.read().await;
|
||||
cache.len()
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,15 @@ use tokio::fs::{self, File, OpenOptions};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::ports::chunked_upload_ports::{
|
||||
ChunkedUploadPort,
|
||||
CreateUploadResponseDto,
|
||||
ChunkUploadResponseDto,
|
||||
UploadStatusResponseDto,
|
||||
};
|
||||
use crate::domain::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Minimum file size to use chunked upload (10MB)
|
||||
pub const CHUNKED_UPLOAD_THRESHOLD: usize = 10 * 1024 * 1024;
|
||||
@@ -507,6 +516,93 @@ impl ChunkedUploadService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
#[async_trait]
|
||||
impl ChunkedUploadPort for ChunkedUploadService {
|
||||
async fn create_session(
|
||||
&self,
|
||||
filename: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
total_size: u64,
|
||||
chunk_size: Option<usize>,
|
||||
) -> Result<CreateUploadResponseDto, DomainError> {
|
||||
let resp = self.create_session(filename, folder_id, content_type, total_size, chunk_size).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))?;
|
||||
Ok(CreateUploadResponseDto {
|
||||
upload_id: resp.upload_id,
|
||||
chunk_size: resp.chunk_size,
|
||||
total_chunks: resp.total_chunks,
|
||||
expires_at: resp.expires_at,
|
||||
})
|
||||
}
|
||||
|
||||
async fn upload_chunk(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
chunk_index: usize,
|
||||
data: bytes::Bytes,
|
||||
checksum: Option<String>,
|
||||
) -> Result<ChunkUploadResponseDto, DomainError> {
|
||||
let resp = self.upload_chunk(upload_id, chunk_index, data, checksum).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))?;
|
||||
Ok(ChunkUploadResponseDto {
|
||||
chunk_index: resp.chunk_index,
|
||||
bytes_received: resp.bytes_received,
|
||||
progress: resp.progress,
|
||||
is_complete: resp.is_complete,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_status(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<UploadStatusResponseDto, DomainError> {
|
||||
let resp = self.get_status(upload_id).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::NotFound, "ChunkedUpload", e))?;
|
||||
Ok(UploadStatusResponseDto {
|
||||
upload_id: resp.upload_id,
|
||||
filename: resp.filename,
|
||||
total_size: resp.total_size,
|
||||
bytes_received: resp.bytes_received,
|
||||
progress: resp.progress,
|
||||
total_chunks: resp.total_chunks,
|
||||
completed_chunks: resp.completed_chunks,
|
||||
pending_chunks: resp.pending_chunks,
|
||||
is_complete: resp.is_complete,
|
||||
})
|
||||
}
|
||||
|
||||
async fn complete_upload(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<(PathBuf, String, Option<String>, String, u64), DomainError> {
|
||||
self.complete_upload(upload_id).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
||||
}
|
||||
|
||||
async fn finalize_upload(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
self.finalize_upload(upload_id).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
||||
}
|
||||
|
||||
async fn cancel_upload(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
self.cancel_upload(upload_id).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
||||
}
|
||||
|
||||
fn should_use_chunked(&self, size: u64) -> bool {
|
||||
ChunkedUploadService::should_use_chunked(size)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -9,6 +9,11 @@ use flate2::Compression;
|
||||
use flate2::read::GzEncoder as GzEncoderRead;
|
||||
use flate2::bufread::GzDecoder;
|
||||
|
||||
use crate::application::ports::compression_ports::{
|
||||
CompressionPort,
|
||||
CompressionLevel as PortCompressionLevel,
|
||||
};
|
||||
use crate::domain::errors::DomainError;
|
||||
use crate::infrastructure::services::buffer_pool::BufferPool;
|
||||
|
||||
/// Nivel de compresión para ficheros
|
||||
@@ -254,7 +259,7 @@ impl CompressionService for GzipCompressionService {
|
||||
}
|
||||
|
||||
// Compress collected data
|
||||
match self.compress_data(&data, compression_level).await {
|
||||
match CompressionService::compress_data(self, &data, compression_level).await {
|
||||
Ok(compressed) => {
|
||||
// Return compressed data as a single chunk
|
||||
yield Ok(Bytes::from(compressed));
|
||||
@@ -293,7 +298,7 @@ impl CompressionService for GzipCompressionService {
|
||||
}
|
||||
|
||||
// Decompress collected data
|
||||
match self.decompress_data(&compressed_data).await {
|
||||
match CompressionService::decompress_data(self, &compressed_data).await {
|
||||
Ok(decompressed) => {
|
||||
// Return decompressed data as a single chunk
|
||||
yield Ok(Bytes::from(decompressed));
|
||||
@@ -345,6 +350,35 @@ impl CompressionService for GzipCompressionService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
/// Convert application-layer CompressionLevel to infrastructure CompressionLevel.
|
||||
impl From<PortCompressionLevel> for CompressionLevel {
|
||||
fn from(level: PortCompressionLevel) -> Self {
|
||||
match level {
|
||||
PortCompressionLevel::None => CompressionLevel::None,
|
||||
PortCompressionLevel::Fast => CompressionLevel::Fast,
|
||||
PortCompressionLevel::Default => CompressionLevel::Default,
|
||||
PortCompressionLevel::Best => CompressionLevel::Best,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CompressionPort for GzipCompressionService {
|
||||
async fn compress_data(&self, data: &[u8], level: PortCompressionLevel) -> Result<Vec<u8>, DomainError> {
|
||||
CompressionService::compress_data(self, data, level.into()).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn decompress_data(&self, compressed_data: &[u8]) -> Result<Vec<u8>, DomainError> {
|
||||
CompressionService::decompress_data(self, compressed_data).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
fn should_compress(&self, mime_type: &str, size: u64) -> bool {
|
||||
CompressionService::should_compress(self, mime_type, size)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -359,13 +393,13 @@ mod tests {
|
||||
let data = "Hello, world! ".repeat(1000).into_bytes();
|
||||
|
||||
// Comprimir
|
||||
let compressed = service.compress_data(&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
|
||||
assert!(compressed.len() < data.len());
|
||||
|
||||
// Descomprimir
|
||||
let decompressed = service.decompress_data(&compressed).await.unwrap();
|
||||
let decompressed = CompressionService::decompress_data(&service, &compressed).await.unwrap();
|
||||
|
||||
// Verificar que los datos originales se recuperan correctamente
|
||||
assert_eq!(decompressed, data);
|
||||
@@ -396,7 +430,7 @@ mod tests {
|
||||
}).await.unwrap();
|
||||
|
||||
// Descomprimir los datos
|
||||
let decompressed = service.decompress_data(&compressed_bytes).await.unwrap();
|
||||
let decompressed = CompressionService::decompress_data(&service, &compressed_bytes).await.unwrap();
|
||||
|
||||
// Verificar resultado
|
||||
let expected = "Hello, world! This is a test of streaming compression.";
|
||||
@@ -408,16 +442,16 @@ mod tests {
|
||||
let service = GzipCompressionService::new();
|
||||
|
||||
// Casos que no deberían comprimirse
|
||||
assert!(!service.should_compress("image/jpeg", 100 * 1024));
|
||||
assert!(!service.should_compress("video/mp4", 10 * 1024 * 1024));
|
||||
assert!(!service.should_compress("application/zip", 5 * 1024 * 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, "application/zip", 5 * 1024 * 1024));
|
||||
|
||||
// Casos que sí deberían comprimirse
|
||||
assert!(service.should_compress("text/html", 100 * 1024));
|
||||
assert!(service.should_compress("application/json", 200 * 1024));
|
||||
assert!(service.should_compress("text/plain", 1024 * 1024));
|
||||
assert!(CompressionService::should_compress(&service, "text/html", 100 * 1024));
|
||||
assert!(CompressionService::should_compress(&service, "application/json", 200 * 1024));
|
||||
assert!(CompressionService::should_compress(&service, "text/plain", 1024 * 1024));
|
||||
|
||||
// Archivos pequeños no deberían comprimirse independientemente del tipo
|
||||
assert!(!service.should_compress("text/html", 10 * 1024));
|
||||
assert!(!CompressionService::should_compress(&service, "text/html", 10 * 1024));
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
//! to the same physical blob.
|
||||
//!
|
||||
//! Architecture:
|
||||
//! ```
|
||||
//! ```text
|
||||
//! ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
//! │ User Files │────▶│ Dedup Index │────▶│ Blob Store │
|
||||
//! │ (references) │ │ (hash→metadata) │ │ (actual data) │
|
||||
@@ -26,6 +26,15 @@ use tokio::sync::RwLock;
|
||||
use sha2::{Sha256, Digest};
|
||||
use bytes::Bytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::ports::dedup_ports::{
|
||||
DedupPort,
|
||||
BlobMetadataDto,
|
||||
DedupResultDto,
|
||||
DedupStatsDto,
|
||||
};
|
||||
use crate::domain::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Chunk size for streaming hash calculation (256KB)
|
||||
const HASH_CHUNK_SIZE: usize = 256 * 1024;
|
||||
@@ -664,6 +673,120 @@ impl DedupService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
/// Convert infra DedupResult to port DedupResultDto.
|
||||
impl From<DedupResult> for DedupResultDto {
|
||||
fn from(result: DedupResult) -> Self {
|
||||
match result {
|
||||
DedupResult::NewBlob { hash, size, blob_path } => {
|
||||
DedupResultDto::NewBlob { hash, size, blob_path }
|
||||
}
|
||||
DedupResult::ExistingBlob { hash, size, blob_path, saved_bytes } => {
|
||||
DedupResultDto::ExistingBlob { hash, size, blob_path, saved_bytes }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert infra BlobMetadata to port BlobMetadataDto.
|
||||
impl From<BlobMetadata> for BlobMetadataDto {
|
||||
fn from(m: BlobMetadata) -> Self {
|
||||
BlobMetadataDto {
|
||||
hash: m.hash,
|
||||
size: m.size,
|
||||
ref_count: m.ref_count,
|
||||
content_type: m.content_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert infra DedupStats to port DedupStatsDto.
|
||||
impl From<DedupStats> for DedupStatsDto {
|
||||
fn from(s: DedupStats) -> Self {
|
||||
DedupStatsDto {
|
||||
total_blobs: s.total_blobs,
|
||||
total_bytes_stored: s.total_bytes_stored,
|
||||
total_bytes_referenced: s.total_bytes_referenced,
|
||||
bytes_saved: s.bytes_saved,
|
||||
dedup_hits: s.dedup_hits,
|
||||
dedup_ratio: s.dedup_ratio,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DedupPort for DedupService {
|
||||
async fn store_bytes(
|
||||
&self,
|
||||
content: &[u8],
|
||||
content_type: Option<String>,
|
||||
) -> Result<DedupResultDto, DomainError> {
|
||||
self.store_bytes(content, content_type).await
|
||||
.map(Into::into)
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Dedup", e))
|
||||
}
|
||||
|
||||
async fn store_from_file(
|
||||
&self,
|
||||
source_path: &Path,
|
||||
content_type: Option<String>,
|
||||
) -> Result<DedupResultDto, DomainError> {
|
||||
self.store_from_file(source_path, content_type).await
|
||||
.map(Into::into)
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Dedup", e))
|
||||
}
|
||||
|
||||
async fn blob_exists(&self, hash: &str) -> bool {
|
||||
self.blob_exists(hash).await
|
||||
}
|
||||
|
||||
async fn get_blob_metadata(&self, hash: &str) -> Option<BlobMetadataDto> {
|
||||
self.get_blob_metadata(hash).await.map(Into::into)
|
||||
}
|
||||
|
||||
async fn read_blob(&self, hash: &str) -> Result<Vec<u8>, DomainError> {
|
||||
self.read_blob(hash).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::NotFound, "Blob", e))
|
||||
}
|
||||
|
||||
async fn read_blob_bytes(&self, hash: &str) -> Result<Bytes, DomainError> {
|
||||
self.read_blob_bytes(hash).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::NotFound, "Blob", e))
|
||||
}
|
||||
|
||||
async fn add_reference(&self, hash: &str) -> Result<(), DomainError> {
|
||||
self.add_reference(hash).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Blob", e))
|
||||
}
|
||||
|
||||
async fn remove_reference(&self, hash: &str) -> Result<bool, DomainError> {
|
||||
self.remove_reference(hash).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Blob", e))
|
||||
}
|
||||
|
||||
fn hash_bytes(&self, content: &[u8]) -> String {
|
||||
DedupService::hash_bytes(content)
|
||||
}
|
||||
|
||||
async fn hash_file(&self, path: &Path) -> Result<String, DomainError> {
|
||||
DedupService::hash_file(path).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> DedupStatsDto {
|
||||
self.get_stats().await.into()
|
||||
}
|
||||
|
||||
async fn flush(&self) -> Result<(), DomainError> {
|
||||
self.flush().await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn verify_integrity(&self) -> Result<Vec<String>, DomainError> {
|
||||
self.verify_integrity().await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Dedup", e))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -675,7 +798,8 @@ mod tests {
|
||||
let service = DedupService::new(temp_dir.path());
|
||||
service.initialize().await.unwrap();
|
||||
|
||||
let content = b"Hello, World! This is test content.";
|
||||
// Content must be >= MIN_DEDUP_SIZE (4096 bytes) for dedup to kick in
|
||||
let content = &b"Hello, World! This is test content for dedup. ".repeat(100);
|
||||
|
||||
// First store
|
||||
let result1 = service.store_bytes(content, None).await.unwrap();
|
||||
@@ -698,7 +822,8 @@ mod tests {
|
||||
let service = DedupService::new(temp_dir.path());
|
||||
service.initialize().await.unwrap();
|
||||
|
||||
let content = b"Test content for reference counting";
|
||||
// Content must be >= MIN_DEDUP_SIZE (4096 bytes) for dedup to kick in
|
||||
let content = &b"Test content for reference counting. ".repeat(120);
|
||||
|
||||
// Store twice
|
||||
let result1 = service.store_bytes(content, None).await.unwrap();
|
||||
|
||||
@@ -213,6 +213,34 @@ pub struct CacheStats {
|
||||
/// Thread-safe wrapper for sharing across handlers
|
||||
pub type SharedFileContentCache = Arc<FileContentCache>;
|
||||
|
||||
// ─── ContentCachePort implementation ─────────────────────────
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::application::ports::cache_ports::ContentCachePort;
|
||||
|
||||
#[async_trait]
|
||||
impl ContentCachePort for FileContentCache {
|
||||
fn should_cache(&self, size: usize) -> bool {
|
||||
FileContentCache::should_cache(self, size)
|
||||
}
|
||||
|
||||
async fn get(&self, file_id: &str) -> Option<(Bytes, String, String)> {
|
||||
FileContentCache::get(self, file_id).await
|
||||
}
|
||||
|
||||
async fn put(&self, file_id: String, content: Bytes, etag: String, content_type: String) {
|
||||
FileContentCache::put(self, file_id, content, etag, content_type).await
|
||||
}
|
||||
|
||||
async fn invalidate(&self, file_id: &str) {
|
||||
FileContentCache::invalidate(self, file_id).await
|
||||
}
|
||||
|
||||
async fn clear(&self) {
|
||||
FileContentCache::clear(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -598,6 +598,56 @@ impl FileMetadataCache {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── MetadataCachePort implementation ────────────────────────
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::application::ports::cache_ports::{MetadataCachePort, CachedMetadataDto};
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
#[async_trait]
|
||||
impl MetadataCachePort for FileMetadataCache {
|
||||
async fn get_metadata(&self, path: &Path) -> Option<CachedMetadataDto> {
|
||||
// Delegate to the existing rich get_metadata, then project into the DTO.
|
||||
let fm = FileMetadataCache::get_metadata(self, path).await?;
|
||||
Some(CachedMetadataDto {
|
||||
path: fm.path,
|
||||
exists: fm.exists,
|
||||
is_file: fm.entry_type == CacheEntryType::File,
|
||||
size: fm.size,
|
||||
mime_type: fm.mime_type,
|
||||
created_at: fm.created_at,
|
||||
modified_at: fm.modified_at,
|
||||
})
|
||||
}
|
||||
|
||||
async fn is_file(&self, path: &Path) -> Option<bool> {
|
||||
FileMetadataCache::is_file(self, path).await
|
||||
}
|
||||
|
||||
async fn refresh_metadata(&self, path: &Path) -> Result<CachedMetadataDto, DomainError> {
|
||||
let fm = FileMetadataCache::refresh_metadata(self, path)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("MetadataCache", e.to_string()))?;
|
||||
Ok(CachedMetadataDto {
|
||||
path: fm.path,
|
||||
exists: fm.exists,
|
||||
is_file: fm.entry_type == CacheEntryType::File,
|
||||
size: fm.size,
|
||||
mime_type: fm.mime_type,
|
||||
created_at: fm.created_at,
|
||||
modified_at: fm.modified_at,
|
||||
})
|
||||
}
|
||||
|
||||
async fn invalidate(&self, path: &Path) {
|
||||
FileMetadataCache::invalidate(self, path).await
|
||||
}
|
||||
|
||||
async fn invalidate_directory(&self, dir_path: &Path) {
|
||||
FileMetadataCache::invalidate_directory(self, dir_path).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -648,10 +698,12 @@ mod tests {
|
||||
async fn test_directory_operations() {
|
||||
// Crear estructura de directorios para pruebas
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let sub_dir = temp_dir.path().join("subdir");
|
||||
// Canonicalize to handle macOS /var -> /private/var symlinks
|
||||
let base_path = temp_dir.path().canonicalize().unwrap();
|
||||
let sub_dir = base_path.join("subdir");
|
||||
fs::create_dir(&sub_dir).await.unwrap();
|
||||
|
||||
let file1 = temp_dir.path().join("file1.txt");
|
||||
let file1 = base_path.join("file1.txt");
|
||||
let file2 = sub_dir.join("file2.txt");
|
||||
|
||||
File::create(&file1).await.unwrap();
|
||||
@@ -662,20 +714,19 @@ mod tests {
|
||||
let cache = FileMetadataCache::new(config, 1000);
|
||||
|
||||
// Precargar directorio recursivamente
|
||||
let count = cache.preload_directory(temp_dir.path(), true, 2).await.unwrap();
|
||||
assert_eq!(count, 3); // dir, subdir, 2 files
|
||||
// preload_directory caches the *contents* of the directory, not the root itself
|
||||
let count = cache.preload_directory(&base_path, true, 2).await.unwrap();
|
||||
assert_eq!(count, 3); // subdir, file1, file2
|
||||
|
||||
// Verificar existencia en caché
|
||||
assert_eq!(cache.is_dir(temp_dir.path()).await, Some(true));
|
||||
// Verificar existencia en caché (solo contenido, no la raíz)
|
||||
assert_eq!(cache.is_dir(&sub_dir).await, Some(true));
|
||||
assert_eq!(cache.is_file(&file1).await, Some(true));
|
||||
assert_eq!(cache.is_file(&file2).await, Some(true));
|
||||
|
||||
// Invalidar directorio y contenido
|
||||
cache.invalidate_directory(temp_dir.path()).await;
|
||||
cache.invalidate_directory(&base_path).await;
|
||||
|
||||
// Verificar que nada existe en caché
|
||||
assert!(cache.exists(temp_dir.path()).await.is_none());
|
||||
assert!(cache.exists(&sub_dir).await.is_none());
|
||||
assert!(cache.exists(&file1).await.is_none());
|
||||
assert!(cache.exists(&file2).await.is_none());
|
||||
|
||||
@@ -515,6 +515,7 @@ impl Clone for IdMappingService {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -18,6 +18,14 @@ use bytes::Bytes;
|
||||
use lru::LruCache;
|
||||
use std::num::NonZeroUsize;
|
||||
use image::{ImageFormat, DynamicImage};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::ports::transcode_ports::{
|
||||
ImageTranscodePort,
|
||||
OutputFormat as PortOutputFormat,
|
||||
TranscodeStatsDto,
|
||||
};
|
||||
use crate::domain::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Maximum file size for transcoding (5MB - larger files stream directly)
|
||||
pub const MAX_TRANSCODE_SIZE: u64 = 5 * 1024 * 1024;
|
||||
@@ -354,6 +362,59 @@ impl ImageTranscodeService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
/// Convert port OutputFormat to infra OutputFormat.
|
||||
impl From<PortOutputFormat> for OutputFormat {
|
||||
fn from(fmt: PortOutputFormat) -> Self {
|
||||
match fmt {
|
||||
PortOutputFormat::WebP => OutputFormat::WebP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ImageTranscodePort for ImageTranscodeService {
|
||||
fn can_transcode(&self, mime_type: &str) -> bool {
|
||||
ImageTranscodeService::can_transcode(mime_type)
|
||||
}
|
||||
|
||||
fn should_transcode(&self, mime_type: &str, file_size: u64) -> bool {
|
||||
ImageTranscodeService::should_transcode(mime_type, file_size)
|
||||
}
|
||||
|
||||
async fn get_transcoded(
|
||||
&self,
|
||||
file_id: &str,
|
||||
original_content: &[u8],
|
||||
original_mime: &str,
|
||||
target_format: PortOutputFormat,
|
||||
) -> Result<(Bytes, String, bool), DomainError> {
|
||||
self.get_transcoded(file_id, original_content, original_mime, target_format.into())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ImageTranscode", e))
|
||||
}
|
||||
|
||||
async fn invalidate(&self, file_id: &str) {
|
||||
self.invalidate(file_id).await
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> TranscodeStatsDto {
|
||||
let stats = self.get_stats().await;
|
||||
TranscodeStatsDto {
|
||||
cache_hits: stats.cache_hits,
|
||||
disk_hits: stats.disk_hits,
|
||||
transcodes: stats.transcodes,
|
||||
bytes_saved: stats.bytes_saved,
|
||||
transcode_errors: stats.transcode_errors,
|
||||
}
|
||||
}
|
||||
|
||||
async fn clear_cache(&self) -> Result<(), DomainError> {
|
||||
self.clear_cache().await.map_err(DomainError::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -2,7 +2,6 @@ pub mod file_system_i18n_service;
|
||||
pub mod file_system_utils;
|
||||
pub mod id_mapping_service;
|
||||
pub mod id_mapping_optimizer;
|
||||
pub mod cache_manager;
|
||||
pub mod file_metadata_cache;
|
||||
pub mod file_content_cache;
|
||||
pub mod compression_service;
|
||||
|
||||
@@ -19,6 +19,14 @@ use image::{ImageFormat, imageops::FilterType};
|
||||
use lru::LruCache;
|
||||
use std::num::NonZeroUsize;
|
||||
use bytes::Bytes;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::ports::thumbnail_ports::{
|
||||
ThumbnailPort,
|
||||
ThumbnailSize as PortThumbnailSize,
|
||||
ThumbnailStatsDto,
|
||||
};
|
||||
use crate::domain::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Thumbnail sizes supported by the system
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@@ -329,6 +337,60 @@ impl ThumbnailService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
/// Convert port ThumbnailSize to infra ThumbnailSize.
|
||||
impl From<PortThumbnailSize> for ThumbnailSize {
|
||||
fn from(size: PortThumbnailSize) -> Self {
|
||||
match size {
|
||||
PortThumbnailSize::Icon => ThumbnailSize::Icon,
|
||||
PortThumbnailSize::Preview => ThumbnailSize::Preview,
|
||||
PortThumbnailSize::Large => ThumbnailSize::Large,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ThumbnailPort for ThumbnailService {
|
||||
fn is_supported_image(&self, mime_type: &str) -> bool {
|
||||
ThumbnailService::is_supported_image(mime_type)
|
||||
}
|
||||
|
||||
async fn get_thumbnail(
|
||||
&self,
|
||||
file_id: &str,
|
||||
size: PortThumbnailSize,
|
||||
original_path: &Path,
|
||||
) -> Result<Bytes, DomainError> {
|
||||
self.get_thumbnail(file_id, size.into(), original_path)
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Thumbnail", e.to_string()))
|
||||
}
|
||||
|
||||
fn generate_all_sizes_background(
|
||||
self: Arc<Self>,
|
||||
file_id: String,
|
||||
original_path: PathBuf,
|
||||
) {
|
||||
ThumbnailService::generate_all_sizes_background(self, file_id, original_path)
|
||||
}
|
||||
|
||||
async fn delete_thumbnails(&self, file_id: &str) -> Result<(), DomainError> {
|
||||
self.delete_thumbnails(file_id)
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Thumbnail", e.to_string()))
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> ThumbnailStatsDto {
|
||||
let stats = self.get_stats().await;
|
||||
ThumbnailStatsDto {
|
||||
cached_thumbnails: stats.cached_thumbnails,
|
||||
cache_size_bytes: stats.cache_size_bytes,
|
||||
max_cache_bytes: stats.max_cache_bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Thumbnail service errors
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ThumbnailError {
|
||||
|
||||
@@ -78,8 +78,8 @@ impl TrashCleanupService {
|
||||
|
||||
// Eliminar cada elemento expirado
|
||||
for item in expired_items {
|
||||
let trash_id = item.id.to_string();
|
||||
let user_id = item.user_id.to_string();
|
||||
let trash_id = item.id().to_string();
|
||||
let user_id = item.user_id().to_string();
|
||||
|
||||
debug!("Eliminando elemento expirado: id={}, user={}", trash_id, user_id);
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ use tokio::sync::{RwLock, mpsc};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use bytes::Bytes;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::ports::cache_ports::{WriteBehindCachePort, WriteBehindStatsDto};
|
||||
use crate::domain::errors::DomainError;
|
||||
|
||||
/// Maximum size for write-behind cache (files larger bypass cache)
|
||||
const WRITE_BEHIND_MAX_SIZE: usize = 1024 * 1024; // 1MB
|
||||
@@ -364,6 +368,56 @@ impl WriteBehindCache {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
#[async_trait]
|
||||
impl WriteBehindCachePort for WriteBehindCache {
|
||||
fn is_eligible_size(&self, size: usize) -> bool {
|
||||
WriteBehindCache::is_eligible(size)
|
||||
}
|
||||
|
||||
async fn put_pending(
|
||||
&self,
|
||||
file_id: String,
|
||||
content: Bytes,
|
||||
target_path: PathBuf,
|
||||
) -> Result<bool, DomainError> {
|
||||
self.put_pending(file_id, content, target_path).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_pending(&self, file_id: &str) -> Option<Bytes> {
|
||||
self.get_pending(file_id).await
|
||||
}
|
||||
|
||||
async fn is_pending(&self, file_id: &str) -> bool {
|
||||
self.is_pending(file_id).await
|
||||
}
|
||||
|
||||
async fn force_flush(&self, file_id: &str) -> Result<(), DomainError> {
|
||||
self.force_flush(file_id).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn flush_all(&self) -> Result<(), DomainError> {
|
||||
self.flush_all().await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<(), DomainError> {
|
||||
self.shutdown().await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> WriteBehindStatsDto {
|
||||
let stats = self.get_stats().await;
|
||||
WriteBehindStatsDto {
|
||||
pending_count: stats.pending_count,
|
||||
pending_bytes: stats.pending_bytes,
|
||||
total_writes: stats.total_writes,
|
||||
total_bytes_written: stats.total_bytes_written,
|
||||
cache_hits: stats.cache_hits,
|
||||
avg_flush_time_us: stats.avg_flush_time_us,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WriteBehindCache {
|
||||
fn default() -> Self {
|
||||
// Note: This creates a non-Arc version, prefer using new()
|
||||
|
||||
@@ -2,10 +2,13 @@ use std::io::{Cursor, Read, Write};
|
||||
use zip::{ZipWriter, write::SimpleFileOptions};
|
||||
use thiserror::Error;
|
||||
use tracing::*;
|
||||
use async_trait::async_trait;
|
||||
use crate::{
|
||||
application::dtos::file_dto::FileDto,
|
||||
application::dtos::folder_dto::FolderDto,
|
||||
application::ports::inbound::{FileUseCase, FolderUseCase},
|
||||
application::ports::inbound::FolderUseCase,
|
||||
application::ports::file_ports::FileRetrievalUseCase,
|
||||
application::ports::zip_ports::ZipPort,
|
||||
common::errors::{Result, DomainError, ErrorKind},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
@@ -45,13 +48,13 @@ impl From<zip::result::ZipError> for DomainError {
|
||||
|
||||
/// Servicio para crear archivos ZIP
|
||||
pub struct ZipService {
|
||||
file_service: Arc<dyn FileUseCase>,
|
||||
file_service: Arc<dyn FileRetrievalUseCase>,
|
||||
folder_service: Arc<dyn FolderUseCase>,
|
||||
}
|
||||
|
||||
impl ZipService {
|
||||
/// Crea una nueva instancia del servicio ZIP con una referencia al servicio de archivos
|
||||
pub fn new(file_service: Arc<dyn FileUseCase>, folder_service: Arc<dyn FolderUseCase>) -> Self {
|
||||
pub fn new(file_service: Arc<dyn FileRetrievalUseCase>, folder_service: Arc<dyn FolderUseCase>) -> Self {
|
||||
Self {
|
||||
file_service,
|
||||
folder_service,
|
||||
@@ -225,4 +228,17 @@ impl ZipService {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
#[async_trait]
|
||||
impl ZipPort for ZipService {
|
||||
async fn create_folder_zip(
|
||||
&self,
|
||||
folder_id: &str,
|
||||
folder_name: &str,
|
||||
) -> std::result::Result<Vec<u8>, DomainError> {
|
||||
self.create_folder_zip(folder_id, folder_name).await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user