refactor: remove serde from domain entities for Clean Architecture compliance
- Remove Serialize/Deserialize from File, Folder, Session, User, Contact entities - Create contact_persistence_dto.rs for JSONB persistence in infrastructure layer - Update contact_pg_repository to use persistence DTOs - Fix dependency on zip crate (downgrade from 7.2.0 to 2.1.0) - Fix unused variable warnings in main.rs - Move PathService import from domain to infrastructure - Add missing fields to CoreServices and RepositoryServices - Create proper service initialization in main.rs Clean Architecture improvements: - Domain layer no longer depends on serde framework - Persistence concerns isolated to infrastructure layer - TokenClaims in auth_service.rs is only exception (required for JWT)
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
//! Calendar Storage Adapter
|
||||
//!
|
||||
//! This adapter implements the `CalendarStoragePort` application port using
|
||||
//! the `CalendarRepository` and `CalendarEventRepository` domain repositories.
|
||||
//! It bridges the gap between the application layer and the infrastructure layer.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::calendar_dto::{
|
||||
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto,
|
||||
CreateEventDto, UpdateEventDto, CreateEventICalDto
|
||||
};
|
||||
use crate::application::ports::calendar_ports::CalendarStoragePort;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::domain::entities::calendar::Calendar;
|
||||
use crate::domain::entities::calendar_event::CalendarEvent;
|
||||
use crate::domain::repositories::calendar_repository::CalendarRepository;
|
||||
use crate::domain::repositories::calendar_event_repository::CalendarEventRepository;
|
||||
|
||||
/// Adapter that implements CalendarStoragePort using domain repositories
|
||||
pub struct CalendarStorageAdapter {
|
||||
calendar_repository: Arc<dyn CalendarRepository>,
|
||||
event_repository: Arc<dyn CalendarEventRepository>,
|
||||
}
|
||||
|
||||
impl CalendarStorageAdapter {
|
||||
/// Creates a new CalendarStorageAdapter with the given repositories
|
||||
pub fn new(
|
||||
calendar_repository: Arc<dyn CalendarRepository>,
|
||||
event_repository: Arc<dyn CalendarEventRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
calendar_repository,
|
||||
event_repository,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CalendarStoragePort for CalendarStorageAdapter {
|
||||
// Calendar operations
|
||||
|
||||
async fn create_calendar(&self, dto: CreateCalendarDto, owner_id: &str) -> Result<CalendarDto, DomainError> {
|
||||
let calendar = Calendar::new(
|
||||
dto.name,
|
||||
owner_id.to_string(),
|
||||
dto.description,
|
||||
dto.color,
|
||||
)?;
|
||||
|
||||
let created = self.calendar_repository.create_calendar(calendar).await?;
|
||||
Ok(CalendarDto::from(created))
|
||||
}
|
||||
|
||||
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
let mut calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?;
|
||||
|
||||
if let Some(name) = update.name {
|
||||
calendar.update_name(name)?;
|
||||
}
|
||||
if let Some(description) = update.description {
|
||||
calendar.update_description(Some(description));
|
||||
}
|
||||
if let Some(color) = update.color {
|
||||
calendar.update_color(Some(color))?;
|
||||
}
|
||||
|
||||
let updated = self.calendar_repository.update_calendar(calendar).await?;
|
||||
Ok(CalendarDto::from(updated))
|
||||
}
|
||||
|
||||
async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
// First delete all events in the calendar
|
||||
self.event_repository.delete_all_events_in_calendar(&uuid).await?;
|
||||
|
||||
// Then delete the calendar itself
|
||||
self.calendar_repository.delete_calendar(&uuid).await
|
||||
}
|
||||
|
||||
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
let calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?;
|
||||
Ok(CalendarDto::from(calendar))
|
||||
}
|
||||
|
||||
async fn list_calendars_by_owner(&self, owner_id: &str) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
let calendars = self.calendar_repository.list_calendars_by_owner(owner_id).await?;
|
||||
Ok(calendars.into_iter().map(CalendarDto::from).collect())
|
||||
}
|
||||
|
||||
async fn list_calendars_shared_with_user(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
let calendars = self.calendar_repository.list_calendars_shared_with_user(user_id).await?;
|
||||
Ok(calendars.into_iter().map(CalendarDto::from).collect())
|
||||
}
|
||||
|
||||
async fn list_public_calendars(&self, limit: i64, offset: i64) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
let calendars = self.calendar_repository.list_public_calendars(limit, offset).await?;
|
||||
Ok(calendars.into_iter().map(CalendarDto::from).collect())
|
||||
}
|
||||
|
||||
async fn check_calendar_access(&self, calendar_id: &str, user_id: &str) -> Result<bool, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
self.calendar_repository.user_has_calendar_access(&uuid, user_id).await
|
||||
}
|
||||
|
||||
// Calendar sharing
|
||||
|
||||
async fn share_calendar(&self, calendar_id: &str, user_id: &str, access_level: &str) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
self.calendar_repository.share_calendar(&uuid, user_id, access_level).await
|
||||
}
|
||||
|
||||
async fn remove_calendar_sharing(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
self.calendar_repository.remove_calendar_sharing(&uuid, user_id).await
|
||||
}
|
||||
|
||||
async fn get_calendar_shares(&self, calendar_id: &str) -> Result<Vec<(String, String)>, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
self.calendar_repository.get_calendar_shares(&uuid).await
|
||||
}
|
||||
|
||||
// Calendar properties
|
||||
|
||||
async fn set_calendar_property(&self, calendar_id: &str, property_name: &str, property_value: &str) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
self.calendar_repository.set_calendar_property(&uuid, property_name, property_value).await
|
||||
}
|
||||
|
||||
async fn get_calendar_property(&self, calendar_id: &str, property_name: &str) -> Result<Option<String>, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
self.calendar_repository.get_calendar_property(&uuid, property_name).await
|
||||
}
|
||||
|
||||
async fn get_calendar_properties(&self, calendar_id: &str) -> Result<HashMap<String, String>, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
self.calendar_repository.get_calendar_properties(&uuid).await
|
||||
}
|
||||
|
||||
// Event operations
|
||||
|
||||
async fn create_event(&self, dto: CreateEventDto) -> Result<CalendarEventDto, DomainError> {
|
||||
let calendar_id = Uuid::parse_str(&dto.calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid calendar ID format"))?;
|
||||
|
||||
// Verify calendar exists and user has access
|
||||
let _calendar = self.calendar_repository.find_calendar_by_id(&calendar_id).await?;
|
||||
|
||||
// Generate basic iCal data
|
||||
let ical_data = format!(
|
||||
"BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//OxiCloud//EN\nBEGIN:VEVENT\nUID:{}@oxicloud\nDTSTAMP:{}\nDTSTART:{}\nDTEND:{}\nSUMMARY:{}\nEND:VEVENT\nEND:VCALENDAR",
|
||||
uuid::Uuid::new_v4(),
|
||||
chrono::Utc::now().format("%Y%m%dT%H%M%SZ"),
|
||||
dto.start_time.format("%Y%m%dT%H%M%SZ"),
|
||||
dto.end_time.format("%Y%m%dT%H%M%SZ"),
|
||||
dto.summary
|
||||
);
|
||||
|
||||
let event = CalendarEvent::new(
|
||||
calendar_id,
|
||||
dto.summary,
|
||||
dto.description,
|
||||
dto.location,
|
||||
dto.start_time,
|
||||
dto.end_time,
|
||||
dto.all_day.unwrap_or(false),
|
||||
dto.rrule,
|
||||
ical_data,
|
||||
)?;
|
||||
|
||||
let created = self.event_repository.create_event(event).await?;
|
||||
Ok(CalendarEventDto::from(created))
|
||||
}
|
||||
|
||||
async fn create_event_from_ical(&self, dto: CreateEventICalDto) -> Result<CalendarEventDto, DomainError> {
|
||||
let calendar_id = Uuid::parse_str(&dto.calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid calendar ID format"))?;
|
||||
|
||||
// Verify calendar exists
|
||||
let _calendar = self.calendar_repository.find_calendar_by_id(&calendar_id).await?;
|
||||
|
||||
// Parse iCal data and create event
|
||||
let event = CalendarEvent::from_ical(calendar_id, dto.ical_data.clone())?;
|
||||
|
||||
let created = self.event_repository.create_event(event).await?;
|
||||
Ok(CalendarEventDto::from(created))
|
||||
}
|
||||
|
||||
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError> {
|
||||
let uuid = Uuid::parse_str(event_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?;
|
||||
|
||||
let mut event = self.event_repository.find_event_by_id(&uuid).await?;
|
||||
|
||||
if let Some(summary) = update.summary {
|
||||
event.update_summary(summary)?;
|
||||
}
|
||||
if let Some(description) = update.description {
|
||||
event.update_description(Some(description));
|
||||
}
|
||||
if let Some(location) = update.location {
|
||||
event.update_location(Some(location));
|
||||
}
|
||||
if let Some(start_time) = update.start_time {
|
||||
if let Some(end_time) = update.end_time {
|
||||
event.update_time_range(start_time, end_time)?;
|
||||
} else {
|
||||
event.update_time_range(start_time, *event.end_time())?;
|
||||
}
|
||||
} else if let Some(end_time) = update.end_time {
|
||||
event.update_time_range(*event.start_time(), end_time)?;
|
||||
}
|
||||
if let Some(all_day) = update.all_day {
|
||||
event.update_all_day(all_day);
|
||||
}
|
||||
if let Some(rrule) = update.rrule {
|
||||
event.update_rrule(Some(rrule))?;
|
||||
}
|
||||
|
||||
let updated = self.event_repository.update_event(event).await?;
|
||||
Ok(CalendarEventDto::from(updated))
|
||||
}
|
||||
|
||||
async fn delete_event(&self, event_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(event_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?;
|
||||
|
||||
self.event_repository.delete_event(&uuid).await
|
||||
}
|
||||
|
||||
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError> {
|
||||
let uuid = Uuid::parse_str(event_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?;
|
||||
|
||||
let event = self.event_repository.find_event_by_id(&uuid).await?;
|
||||
Ok(CalendarEventDto::from(event))
|
||||
}
|
||||
|
||||
async fn list_events_by_calendar(&self, calendar_id: &str) -> Result<Vec<CalendarEventDto>, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
let events = self.event_repository.list_events_by_calendar(&uuid).await?;
|
||||
Ok(events.into_iter().map(CalendarEventDto::from).collect())
|
||||
}
|
||||
|
||||
async fn list_events_by_calendar_paginated(&self, calendar_id: &str, limit: i64, offset: i64) -> Result<Vec<CalendarEventDto>, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
let events = self.event_repository.list_events_by_calendar_paginated(&uuid, limit, offset).await?;
|
||||
Ok(events.into_iter().map(CalendarEventDto::from).collect())
|
||||
}
|
||||
|
||||
async fn get_events_in_time_range(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
start: &DateTime<Utc>,
|
||||
end: &DateTime<Utc>
|
||||
) -> Result<Vec<CalendarEventDto>, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
|
||||
let events = self.event_repository.get_events_in_time_range(&uuid, start, end).await?;
|
||||
Ok(events.into_iter().map(CalendarEventDto::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Tests would go here using mock repositories
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
//! Contact Storage Adapter
|
||||
//!
|
||||
//! This adapter implements the `AddressBookUseCase` and `ContactUseCase` application ports
|
||||
//! using the domain repositories. It bridges the gap between the application layer
|
||||
//! and the infrastructure layer for CardDAV functionality.
|
||||
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::address_book_dto::{
|
||||
AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto,
|
||||
ShareAddressBookDto, UnshareAddressBookDto
|
||||
};
|
||||
use crate::application::dtos::contact_dto::{
|
||||
ContactDto, CreateContactDto, UpdateContactDto, CreateContactVCardDto,
|
||||
ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto,
|
||||
EmailDto, PhoneDto, AddressDto
|
||||
};
|
||||
use crate::application::ports::carddav_ports::{AddressBookUseCase, ContactUseCase};
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::domain::entities::contact::{AddressBook, Contact, ContactGroup, Email, Phone, Address};
|
||||
use crate::domain::repositories::address_book_repository::AddressBookRepository;
|
||||
use crate::domain::repositories::contact_repository::{ContactRepository, ContactGroupRepository};
|
||||
|
||||
/// Adapter that implements AddressBookUseCase and ContactUseCase using domain repositories
|
||||
pub struct ContactStorageAdapter {
|
||||
address_book_repository: Arc<dyn AddressBookRepository>,
|
||||
contact_repository: Arc<dyn ContactRepository>,
|
||||
group_repository: Arc<dyn ContactGroupRepository>,
|
||||
}
|
||||
|
||||
impl ContactStorageAdapter {
|
||||
/// Creates a new ContactStorageAdapter with the given repositories
|
||||
pub fn new(
|
||||
address_book_repository: Arc<dyn AddressBookRepository>,
|
||||
contact_repository: Arc<dyn ContactRepository>,
|
||||
group_repository: Arc<dyn ContactGroupRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
address_book_repository,
|
||||
contact_repository,
|
||||
group_repository,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to parse UUID from string
|
||||
fn parse_uuid(id: &str, entity_name: &'static str) -> Result<Uuid, DomainError> {
|
||||
Uuid::parse_str(id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, entity_name, format!("Invalid {} ID format", entity_name)))
|
||||
}
|
||||
|
||||
/// Helper to check if user has access to an address book
|
||||
async fn check_address_book_access(&self, address_book_id: &Uuid, user_id: &str) -> Result<AddressBook, DomainError> {
|
||||
let address_book = self.address_book_repository
|
||||
.get_address_book_by_id(address_book_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
// Check if user is owner
|
||||
if address_book.owner_id == user_id {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
// Check if address book is public
|
||||
if address_book.is_public {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
// Check if address book is shared with user
|
||||
let shares = self.address_book_repository.get_address_book_shares(address_book_id).await?;
|
||||
if shares.iter().any(|(shared_user, _)| shared_user == user_id) {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Access denied to address book"))
|
||||
}
|
||||
|
||||
/// Helper to check write access
|
||||
async fn check_write_access(&self, address_book_id: &Uuid, user_id: &str) -> Result<AddressBook, DomainError> {
|
||||
let address_book = self.address_book_repository
|
||||
.get_address_book_by_id(address_book_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
// Owner always has write access
|
||||
if address_book.owner_id == user_id {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
// Check shares for write permission
|
||||
let shares = self.address_book_repository.get_address_book_shares(address_book_id).await?;
|
||||
if shares.iter().any(|(shared_user, can_write)| shared_user == user_id && *can_write) {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Write access denied"))
|
||||
}
|
||||
|
||||
/// Convert EmailDto to domain Email
|
||||
fn dto_to_email(dto: EmailDto) -> Email {
|
||||
Email {
|
||||
email: dto.email,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert PhoneDto to domain Phone
|
||||
fn dto_to_phone(dto: PhoneDto) -> Phone {
|
||||
Phone {
|
||||
number: dto.number,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert AddressDto to domain Address
|
||||
fn dto_to_address(dto: AddressDto) -> Address {
|
||||
Address {
|
||||
street: dto.street,
|
||||
city: dto.city,
|
||||
state: dto.state,
|
||||
postal_code: dto.postal_code,
|
||||
country: dto.country,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate vCard from contact data
|
||||
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 {
|
||||
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("");
|
||||
vcard.push_str(&format!("N:{};{};;;\n", last, first));
|
||||
}
|
||||
|
||||
if let Some(ref nickname) = contact.nickname {
|
||||
vcard.push_str(&format!("NICKNAME:{}\n", nickname));
|
||||
}
|
||||
|
||||
for email in &contact.email {
|
||||
vcard.push_str(&format!("EMAIL;TYPE={}:{}\n", email.r#type.to_uppercase(), email.email));
|
||||
}
|
||||
|
||||
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 {
|
||||
vcard.push_str(&format!("ORG:{}\n", org));
|
||||
}
|
||||
|
||||
if let Some(ref title) = contact.title {
|
||||
vcard.push_str(&format!("TITLE:{}\n", title));
|
||||
}
|
||||
|
||||
if let Some(ref notes) = contact.notes {
|
||||
vcard.push_str(&format!("NOTE:{}\n", notes));
|
||||
}
|
||||
|
||||
vcard.push_str(&format!("UID:{}\n", contact.uid));
|
||||
vcard.push_str("END:VCARD\n");
|
||||
|
||||
vcard
|
||||
}
|
||||
}
|
||||
|
||||
#[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 created = self.address_book_repository.create_address_book(address_book).await?;
|
||||
Ok(AddressBookDto::from(created))
|
||||
}
|
||||
|
||||
async fn update_address_book(&self, address_book_id: &str, update: UpdateAddressBookDto) -> Result<AddressBookDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Check write access
|
||||
let mut address_book = self.check_write_access(&uuid, &update.user_id).await?;
|
||||
|
||||
if let Some(name) = update.name {
|
||||
address_book.name = name;
|
||||
}
|
||||
if let Some(description) = update.description {
|
||||
address_book.description = Some(description);
|
||||
}
|
||||
if let Some(color) = update.color {
|
||||
address_book.color = Some(color);
|
||||
}
|
||||
if let Some(is_public) = update.is_public {
|
||||
address_book.is_public = is_public;
|
||||
}
|
||||
address_book.updated_at = chrono::Utc::now();
|
||||
|
||||
let updated = self.address_book_repository.update_address_book(address_book).await?;
|
||||
Ok(AddressBookDto::from(updated))
|
||||
}
|
||||
|
||||
async fn delete_address_book(&self, address_book_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Only owner can delete
|
||||
let address_book = self.address_book_repository
|
||||
.get_address_book_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
if address_book.owner_id != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can delete address book"));
|
||||
}
|
||||
|
||||
self.address_book_repository.delete_address_book(&uuid).await
|
||||
}
|
||||
|
||||
async fn get_address_book(&self, address_book_id: &str, user_id: &str) -> Result<AddressBookDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
let address_book = self.check_address_book_access(&uuid, user_id).await?;
|
||||
Ok(AddressBookDto::from(address_book))
|
||||
}
|
||||
|
||||
async fn list_user_address_books(&self, user_id: &str) -> Result<Vec<AddressBookDto>, DomainError> {
|
||||
let owned = self.address_book_repository.get_address_books_by_owner(user_id).await?;
|
||||
let shared = self.address_book_repository.get_shared_address_books(user_id).await?;
|
||||
|
||||
let mut all_books: Vec<AddressBook> = owned;
|
||||
all_books.extend(shared);
|
||||
|
||||
Ok(all_books.into_iter().map(AddressBookDto::from).collect())
|
||||
}
|
||||
|
||||
async fn list_public_address_books(&self) -> Result<Vec<AddressBookDto>, DomainError> {
|
||||
let public = self.address_book_repository.get_public_address_books().await?;
|
||||
Ok(public.into_iter().map(AddressBookDto::from).collect())
|
||||
}
|
||||
|
||||
async fn share_address_book(&self, dto: ShareAddressBookDto, user_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||
|
||||
// Only owner can share
|
||||
let address_book = self.address_book_repository
|
||||
.get_address_book_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
if address_book.owner_id != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can share"));
|
||||
}
|
||||
|
||||
self.address_book_repository.share_address_book(&uuid, &dto.user_id, dto.can_write).await
|
||||
}
|
||||
|
||||
async fn unshare_address_book(&self, dto: UnshareAddressBookDto, user_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||
|
||||
// Only owner can unshare
|
||||
let address_book = self.address_book_repository
|
||||
.get_address_book_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
if address_book.owner_id != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can unshare"));
|
||||
}
|
||||
|
||||
self.address_book_repository.unshare_address_book(&uuid, &dto.user_id).await
|
||||
}
|
||||
|
||||
async fn get_address_book_shares(&self, address_book_id: &str, user_id: &str) -> Result<Vec<(String, bool)>, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Only owner can view shares
|
||||
let address_book = self.address_book_repository
|
||||
.get_address_book_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
|
||||
if address_book.owner_id != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can view shares"));
|
||||
}
|
||||
|
||||
self.address_book_repository.get_address_book_shares(&uuid).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ContactUseCase for ContactStorageAdapter {
|
||||
async fn create_contact(&self, dto: CreateContactDto) -> Result<ContactDto, DomainError> {
|
||||
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(&address_book_id, &dto.user_id).await?;
|
||||
|
||||
let contact = Contact {
|
||||
id: 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(),
|
||||
};
|
||||
|
||||
// Generate vCard
|
||||
let mut contact_with_vcard = contact;
|
||||
contact_with_vcard.vcard = Self::generate_vcard(&contact_with_vcard);
|
||||
|
||||
let created = self.contact_repository.create_contact(contact_with_vcard).await?;
|
||||
Ok(ContactDto::from(created))
|
||||
}
|
||||
|
||||
async fn create_contact_from_vcard(&self, dto: CreateContactVCardDto) -> Result<ContactDto, DomainError> {
|
||||
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||
|
||||
// Check write access
|
||||
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(),
|
||||
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(),
|
||||
};
|
||||
|
||||
let created = self.contact_repository.create_contact(contact).await?;
|
||||
Ok(ContactDto::from(created))
|
||||
}
|
||||
|
||||
async fn update_contact(&self, contact_id: &str, update: UpdateContactDto) -> Result<ContactDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||
|
||||
let mut contact = self.contact_repository
|
||||
.get_contact_by_id(&uuid)
|
||||
.await?
|
||||
.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?;
|
||||
|
||||
if let Some(full_name) = update.full_name {
|
||||
contact.full_name = Some(full_name);
|
||||
}
|
||||
if let Some(first_name) = update.first_name {
|
||||
contact.first_name = Some(first_name);
|
||||
}
|
||||
if let Some(last_name) = update.last_name {
|
||||
contact.last_name = Some(last_name);
|
||||
}
|
||||
if let Some(nickname) = update.nickname {
|
||||
contact.nickname = Some(nickname);
|
||||
}
|
||||
if let Some(emails) = update.email {
|
||||
contact.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();
|
||||
}
|
||||
if let Some(addresses) = update.address {
|
||||
contact.address = addresses.into_iter().map(Self::dto_to_address).collect();
|
||||
}
|
||||
if let Some(organization) = update.organization {
|
||||
contact.organization = Some(organization);
|
||||
}
|
||||
if let Some(title) = update.title {
|
||||
contact.title = Some(title);
|
||||
}
|
||||
if let Some(notes) = update.notes {
|
||||
contact.notes = Some(notes);
|
||||
}
|
||||
if let Some(photo_url) = update.photo_url {
|
||||
contact.photo_url = Some(photo_url);
|
||||
}
|
||||
if let Some(birthday) = update.birthday {
|
||||
contact.birthday = Some(birthday);
|
||||
}
|
||||
if let Some(anniversary) = update.anniversary {
|
||||
contact.anniversary = Some(anniversary);
|
||||
}
|
||||
|
||||
contact.updated_at = chrono::Utc::now();
|
||||
contact.etag = Uuid::new_v4().to_string();
|
||||
contact.vcard = Self::generate_vcard(&contact);
|
||||
|
||||
let updated = self.contact_repository.update_contact(contact).await?;
|
||||
Ok(ContactDto::from(updated))
|
||||
}
|
||||
|
||||
async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||
|
||||
let contact = self.contact_repository
|
||||
.get_contact_by_id(&uuid)
|
||||
.await?
|
||||
.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.contact_repository.delete_contact(&uuid).await
|
||||
}
|
||||
|
||||
async fn get_contact(&self, contact_id: &str, user_id: &str) -> Result<ContactDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||
|
||||
let contact = self.contact_repository
|
||||
.get_contact_by_id(&uuid)
|
||||
.await?
|
||||
.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?;
|
||||
|
||||
Ok(ContactDto::from(contact))
|
||||
}
|
||||
|
||||
async fn list_contacts(&self, address_book_id: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&uuid, user_id).await?;
|
||||
|
||||
let contacts = self.contact_repository.get_contacts_by_address_book(&uuid).await?;
|
||||
Ok(contacts.into_iter().map(ContactDto::from).collect())
|
||||
}
|
||||
|
||||
async fn search_contacts(&self, address_book_id: &str, query: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&uuid, user_id).await?;
|
||||
|
||||
let contacts = self.contact_repository.search_contacts(&uuid, query).await?;
|
||||
Ok(contacts.into_iter().map(ContactDto::from).collect())
|
||||
}
|
||||
|
||||
async fn create_group(&self, dto: CreateContactGroupDto) -> Result<ContactGroupDto, DomainError> {
|
||||
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(&address_book_id, &dto.user_id).await?;
|
||||
|
||||
let group = ContactGroup {
|
||||
id: Uuid::new_v4(),
|
||||
address_book_id,
|
||||
name: dto.name,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let created = self.group_repository.create_group(group).await?;
|
||||
Ok(ContactGroupDto::from(created))
|
||||
}
|
||||
|
||||
async fn update_group(&self, group_id: &str, update: UpdateContactGroupDto) -> Result<ContactGroupDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
||||
|
||||
let mut group = self.group_repository
|
||||
.get_group_by_id(&uuid)
|
||||
.await?
|
||||
.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?;
|
||||
|
||||
group.name = update.name;
|
||||
group.updated_at = chrono::Utc::now();
|
||||
|
||||
let updated = self.group_repository.update_group(group).await?;
|
||||
Ok(ContactGroupDto::from(updated))
|
||||
}
|
||||
|
||||
async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
||||
|
||||
let group = self.group_repository
|
||||
.get_group_by_id(&uuid)
|
||||
.await?
|
||||
.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.group_repository.delete_group(&uuid).await
|
||||
}
|
||||
|
||||
async fn get_group(&self, group_id: &str, user_id: &str) -> Result<ContactGroupDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
||||
|
||||
let group = self.group_repository
|
||||
.get_group_by_id(&uuid)
|
||||
.await?
|
||||
.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?;
|
||||
|
||||
Ok(ContactGroupDto::from(group))
|
||||
}
|
||||
|
||||
async fn list_groups(&self, address_book_id: &str, user_id: &str) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&uuid, user_id).await?;
|
||||
|
||||
let groups = self.group_repository.get_groups_by_address_book(&uuid).await?;
|
||||
Ok(groups.into_iter().map(ContactGroupDto::from).collect())
|
||||
}
|
||||
|
||||
async fn add_contact_to_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError> {
|
||||
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
|
||||
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
|
||||
|
||||
let group = self.group_repository
|
||||
.get_group_by_id(&group_id)
|
||||
.await?
|
||||
.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.group_repository.add_contact_to_group(&group_id, &contact_id).await
|
||||
}
|
||||
|
||||
async fn remove_contact_from_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError> {
|
||||
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
|
||||
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
|
||||
|
||||
let group = self.group_repository
|
||||
.get_group_by_id(&group_id)
|
||||
.await?
|
||||
.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.group_repository.remove_contact_from_group(&group_id, &contact_id).await
|
||||
}
|
||||
|
||||
async fn list_contacts_in_group(&self, group_id: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError> {
|
||||
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
||||
|
||||
let group = self.group_repository
|
||||
.get_group_by_id(&uuid)
|
||||
.await?
|
||||
.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?;
|
||||
|
||||
let contacts = self.group_repository.get_contacts_in_group(&uuid).await?;
|
||||
Ok(contacts.into_iter().map(ContactDto::from).collect())
|
||||
}
|
||||
|
||||
async fn list_groups_for_contact(&self, contact_id: &str, user_id: &str) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||
|
||||
let contact = self.contact_repository
|
||||
.get_contact_by_id(&uuid)
|
||||
.await?
|
||||
.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?;
|
||||
|
||||
let groups = self.group_repository.get_groups_for_contact(&uuid).await?;
|
||||
Ok(groups.into_iter().map(ContactGroupDto::from).collect())
|
||||
}
|
||||
|
||||
async fn get_contact_vcard(&self, contact_id: &str, user_id: &str) -> Result<String, DomainError> {
|
||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||
|
||||
let contact = self.contact_repository
|
||||
.get_contact_by_id(&uuid)
|
||||
.await?
|
||||
.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?;
|
||||
|
||||
Ok(contact.vcard)
|
||||
}
|
||||
|
||||
async fn get_contacts_as_vcards(&self, address_book_id: &str, user_id: &str) -> Result<Vec<(String, String)>, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&uuid, user_id).await?;
|
||||
|
||||
let contacts = self.contact_repository.get_contacts_by_address_book(&uuid).await?;
|
||||
|
||||
Ok(contacts
|
||||
.into_iter()
|
||||
.map(|c| (c.id.to_string(), c.vcard))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Infrastructure Adapters
|
||||
//!
|
||||
//! This module contains adapters that bridge the gap between domain repositories
|
||||
//! and application ports. These adapters implement the application layer ports
|
||||
//! using the infrastructure layer repositories.
|
||||
|
||||
pub mod calendar_storage_adapter;
|
||||
pub mod contact_storage_adapter;
|
||||
|
||||
pub use calendar_storage_adapter::CalendarStorageAdapter;
|
||||
pub use contact_storage_adapter::ContactStorageAdapter;
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod adapters;
|
||||
pub mod repositories;
|
||||
pub mod services;
|
||||
|
||||
|
||||
@@ -16,39 +16,30 @@ use crate::common::config::AppConfig;
|
||||
|
||||
/// Implementación de repositorio para operaciones de lectura de archivos
|
||||
pub struct FileFsReadRepository {
|
||||
root_path: PathBuf,
|
||||
metadata_manager: Arc<FileMetadataManager>,
|
||||
path_resolver: Arc<FilePathResolver>,
|
||||
config: AppConfig,
|
||||
parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
}
|
||||
|
||||
impl FileFsReadRepository {
|
||||
/// Crea un nuevo repositorio de lectura de archivos
|
||||
pub fn new(
|
||||
root_path: PathBuf,
|
||||
_root_path: PathBuf,
|
||||
metadata_manager: Arc<FileMetadataManager>,
|
||||
path_resolver: Arc<FilePathResolver>,
|
||||
config: AppConfig,
|
||||
parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
_config: AppConfig,
|
||||
_parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
root_path,
|
||||
metadata_manager,
|
||||
path_resolver,
|
||||
config,
|
||||
parallel_processor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un stub para pruebas
|
||||
pub fn default_stub() -> Self {
|
||||
Self {
|
||||
root_path: PathBuf::from("./storage"),
|
||||
metadata_manager: Arc::new(FileMetadataManager::default()),
|
||||
path_resolver: Arc::new(FilePathResolver::default_stub()),
|
||||
config: AppConfig::default(),
|
||||
parallel_processor: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ use crate::application::services::storage_mediator::StorageMediator;
|
||||
// use crate::application::ports::outbound::IdMappingPort;
|
||||
use crate::infrastructure::services::id_mapping_service::IdMappingError;
|
||||
use crate::infrastructure::services::file_metadata_cache::{FileMetadataCache, CacheEntryType};
|
||||
use crate::domain::services::path_service::{StoragePath, PathService};
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::application::ports::outbound::FileStoragePort;
|
||||
@@ -63,7 +64,6 @@ pub struct FileFsRepository {
|
||||
|
||||
impl FileFsRepository {
|
||||
/// Creates a new filesystem-based file repository
|
||||
#[allow(dead_code)]
|
||||
pub fn new(
|
||||
root_path: PathBuf,
|
||||
storage_mediator: Arc<dyn StorageMediator>,
|
||||
@@ -389,14 +389,6 @@ impl From<IdMappingError> for FileRepositoryError {
|
||||
}
|
||||
}
|
||||
|
||||
// Add Timeout variant to FileRepositoryError
|
||||
impl FileRepositoryError {
|
||||
#[allow(dead_code)]
|
||||
fn timeout(message: impl Into<String>) -> Self {
|
||||
FileRepositoryError::Timeout(message.into())
|
||||
}
|
||||
}
|
||||
|
||||
// Errors are already defined by the FileRepositoryError interface
|
||||
|
||||
// Enable cloning for concurrent operations
|
||||
|
||||
@@ -16,43 +16,34 @@ use crate::infrastructure::services::file_system_utils::FileSystemUtils;
|
||||
|
||||
/// Implementación de repositorio para operaciones de escritura de archivos
|
||||
pub struct FileFsWriteRepository {
|
||||
root_path: PathBuf,
|
||||
metadata_manager: Arc<FileMetadataManager>,
|
||||
path_resolver: Arc<FilePathResolver>,
|
||||
storage_mediator: Arc<dyn StorageMediator>,
|
||||
config: AppConfig,
|
||||
parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
}
|
||||
|
||||
impl FileFsWriteRepository {
|
||||
/// Crea un nuevo repositorio de escritura de archivos
|
||||
pub fn new(
|
||||
root_path: PathBuf,
|
||||
_root_path: PathBuf,
|
||||
metadata_manager: Arc<FileMetadataManager>,
|
||||
path_resolver: Arc<FilePathResolver>,
|
||||
storage_mediator: Arc<dyn StorageMediator>,
|
||||
_storage_mediator: Arc<dyn StorageMediator>,
|
||||
config: AppConfig,
|
||||
parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
_parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
root_path,
|
||||
metadata_manager,
|
||||
path_resolver,
|
||||
storage_mediator,
|
||||
config,
|
||||
parallel_processor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un stub para pruebas
|
||||
pub fn default_stub() -> Self {
|
||||
Self {
|
||||
root_path: PathBuf::from("./storage"),
|
||||
metadata_manager: Arc::new(FileMetadataManager::default()),
|
||||
path_resolver: Arc::new(FilePathResolver::default_stub()),
|
||||
storage_mediator: Arc::new(crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub()),
|
||||
config: AppConfig::default(),
|
||||
parallel_processor: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,13 +99,6 @@ impl FileFsWriteRepository {
|
||||
.map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Elimina un archivo de forma no bloqueante
|
||||
async fn delete_file_non_blocking(&self, _abs_path: PathBuf) -> FileRepositoryResult<()> {
|
||||
// Implementación real debe eliminar el archivo
|
||||
// Por ahora, devolvemos OK
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -2,7 +2,8 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::domain::services::path_service::{PathService, StoragePath};
|
||||
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;
|
||||
|
||||
@@ -10,7 +10,8 @@ use crate::domain::entities::folder::{Folder, FolderError};
|
||||
use crate::domain::repositories::folder_repository::{
|
||||
FolderRepository, FolderRepositoryError, FolderRepositoryResult
|
||||
};
|
||||
use crate::domain::services::path_service::{StoragePath, PathService};
|
||||
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;
|
||||
@@ -51,7 +52,6 @@ impl FolderFsRepository {
|
||||
|
||||
/// Creates a stub repository for initialization purposes
|
||||
/// This is used temporarily during dependency injection setup
|
||||
#[allow(dead_code)]
|
||||
pub fn new_stub() -> Self {
|
||||
let root_path = PathBuf::from("/tmp");
|
||||
let path_service = Arc::new(PathService::new(root_path.clone()));
|
||||
|
||||
@@ -422,22 +422,6 @@ impl ParallelFileProcessor {
|
||||
info!("Successfully wrote file of {}MB in parallel with optimized Bytes", file_size / (1024 * 1024));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes a chunk to a file at a specific position
|
||||
#[allow(dead_code)]
|
||||
async fn write_chunk_optimized(
|
||||
file: &mut File,
|
||||
offset: u64,
|
||||
data: Bytes
|
||||
) -> Result<(), std::io::Error> {
|
||||
// Prepare writing at the correct position
|
||||
file.seek(SeekFrom::Start(offset)).await?;
|
||||
|
||||
// Write data without additional copies
|
||||
file.write_all(&data).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::domain::entities::contact::AddressBook;
|
||||
use crate::domain::repositories::address_book_repository::{AddressBookRepository, AddressBookRepositoryResult};
|
||||
use crate::common::errors::{DomainError, ErrorContext};
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
pub struct AddressBookPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
@@ -15,11 +15,6 @@ impl AddressBookPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
// Método auxiliar para mapear errores SQL
|
||||
fn map_error<T>(err: sqlx::Error) -> Result<T, DomainError> {
|
||||
Err(DomainError::database_error(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -409,242 +409,4 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
}
|
||||
|
||||
// Additional methods not part of the trait
|
||||
impl CalendarEventPgRepository {
|
||||
// Helper method to get event by ID
|
||||
async fn get_event_by_id(&self, id: &Uuid) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id, calendar_id, summary, description, location,
|
||||
start_time, end_time, all_day, rrule,
|
||||
created_at, updated_at, ical_uid, ical_data
|
||||
FROM caldav.calendar_events
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by id: {}", e)))?;
|
||||
|
||||
if let Some(row) = row_opt {
|
||||
// En una implementación real, construiríamos un objeto CalendarEvent completo
|
||||
// Este es un ejemplo simplificado
|
||||
let event = CalendarEvent::with_id(
|
||||
row.get("id"),
|
||||
row.get("calendar_id"),
|
||||
row.get("summary"),
|
||||
row.get::<Option<String>, _>("description"),
|
||||
row.get::<Option<String>, _>("location"),
|
||||
row.get("start_time"),
|
||||
row.get("end_time"),
|
||||
row.get("all_day"),
|
||||
row.get::<Option<String>, _>("rrule"),
|
||||
row.get("ical_uid"),
|
||||
row.get("ical_data"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at")
|
||||
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
|
||||
|
||||
return Ok(Some(event));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Helper method to get event by UID
|
||||
async fn get_event_by_uid(&self, calendar_id: &Uuid, uid: &str) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id, calendar_id, summary, description, location,
|
||||
start_time, end_time, all_day, rrule,
|
||||
created_at, updated_at, ical_uid, ical_data
|
||||
FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1 AND ical_uid = $2
|
||||
"#
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(uid)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by UID: {}", e)))?;
|
||||
|
||||
if let Some(_row) = row_opt {
|
||||
// En una implementación real, construiríamos un objeto CalendarEvent a partir de la fila
|
||||
// Por simplicidad, devolvemos None como ejemplo
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Helper method to get events by calendar
|
||||
async fn get_events_by_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id, calendar_id, summary, description, location,
|
||||
start_time, end_time, all_day, rrule,
|
||||
created_at, updated_at, ical_uid, ical_data
|
||||
FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1
|
||||
ORDER BY start_time
|
||||
"#
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get events by calendar: {}", e)))?;
|
||||
|
||||
// En una implementación real, mapearíamos cada fila a un objeto CalendarEvent
|
||||
// Este es un ejemplo simplificado que devuelve una lista vacía
|
||||
let events = Vec::new();
|
||||
|
||||
// Ejemplo de cómo sería el mapeo real:
|
||||
// for row in rows {
|
||||
// let event = CalendarEvent::with_id(
|
||||
// row.get("id"),
|
||||
// row.get("calendar_id"),
|
||||
// row.get("summary"),
|
||||
// // ... otros campos
|
||||
// );
|
||||
// events.push(event);
|
||||
// }
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
// Helper method to get changed events
|
||||
async fn get_changed_events(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
since: &DateTime<Utc>
|
||||
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id, calendar_id, summary, description, location,
|
||||
start_time, end_time, all_day, rrule,
|
||||
created_at, updated_at, ical_uid, ical_data
|
||||
FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1 AND updated_at > $2
|
||||
ORDER BY updated_at
|
||||
"#
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(since)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get changed events: {}", e)))?;
|
||||
|
||||
// En una implementación real, mapearíamos cada fila a un objeto CalendarEvent
|
||||
// Este es un ejemplo simplificado que devuelve una lista vacía
|
||||
let events = Vec::new();
|
||||
|
||||
// Ejemplo de cómo sería el mapeo real:
|
||||
// for row in rows {
|
||||
// let event = CalendarEvent::with_id(
|
||||
// row.get("id"),
|
||||
// row.get("calendar_id"),
|
||||
// row.get("summary"),
|
||||
// row.get::<Option<String>, _>("description"),
|
||||
// row.get::<Option<String>, _>("location"),
|
||||
// row.get("start_time"),
|
||||
// row.get("end_time"),
|
||||
// row.get("all_day"),
|
||||
// row.get::<Option<String>, _>("rrule"),
|
||||
// row.get("ical_uid"),
|
||||
// row.get("ical_data"),
|
||||
// row.get("created_at"),
|
||||
// row.get("updated_at")
|
||||
// ).unwrap();
|
||||
// events.push(event);
|
||||
// }
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
// Helper method to add an attendee to an event
|
||||
async fn add_event_attendee(
|
||||
&self,
|
||||
event_id: &Uuid,
|
||||
email: &str,
|
||||
name: Option<&str>,
|
||||
role: &str,
|
||||
status: &str
|
||||
) -> CalendarEventRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO caldav.calendar_event_attendees (event_id, email, name, role, status)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (event_id, email) DO UPDATE
|
||||
SET name = $3, role = $4, status = $5
|
||||
"#
|
||||
)
|
||||
.bind(event_id)
|
||||
.bind(email)
|
||||
.bind(name)
|
||||
.bind(role)
|
||||
.bind(status)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to add event attendee: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper method to remove an attendee from an event
|
||||
async fn remove_event_attendee(
|
||||
&self,
|
||||
event_id: &Uuid,
|
||||
email: &str
|
||||
) -> CalendarEventRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM caldav.calendar_event_attendees
|
||||
WHERE event_id = $1 AND email = $2
|
||||
"#
|
||||
)
|
||||
.bind(event_id)
|
||||
.bind(email)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to remove event attendee: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper method to get all attendees for an event
|
||||
async fn get_event_attendees(
|
||||
&self,
|
||||
event_id: &Uuid
|
||||
) -> CalendarEventRepositoryResult<Vec<(String, Option<String>, String, String)>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT email, name, role, status
|
||||
FROM caldav.calendar_event_attendees
|
||||
WHERE event_id = $1
|
||||
ORDER BY email
|
||||
"#
|
||||
)
|
||||
.bind(event_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get event attendees: {}", e)))?;
|
||||
|
||||
let mut attendees = Vec::new();
|
||||
for row in rows {
|
||||
let email: String = row.get("email");
|
||||
let name: Option<String> = row.get("name");
|
||||
let role: String = row.get("role");
|
||||
let status: String = row.get("status");
|
||||
attendees.push((email, name, role, status));
|
||||
}
|
||||
|
||||
Ok(attendees)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use sqlx::{PgPool, query, query_as, Row, types::Uuid};
|
||||
use sqlx::{PgPool, Row, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::domain::entities::calendar::Calendar;
|
||||
use crate::domain::repositories::calendar_repository::{CalendarRepository, CalendarRepositoryResult};
|
||||
use crate::common::errors::{DomainError, ErrorContext};
|
||||
use sqlx::Transaction;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
pub struct CalendarPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{PgPool, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::contact::{ContactGroup, Contact};
|
||||
use crate::domain::repositories::contact_repository::{ContactGroupRepository, ContactRepositoryResult};
|
||||
|
||||
pub struct ContactGroupPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl ContactGroupPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ContactGroupRepository for ContactGroupPgRepository {
|
||||
async fn create_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, address_book_id, name, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(group.id)
|
||||
.bind(group.address_book_id)
|
||||
.bind(&group.name)
|
||||
.bind(group.created_at)
|
||||
.bind(group.updated_at)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to create contact group: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Por simplicidad, devolvemos el grupo original
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
UPDATE carddav.contact_groups
|
||||
SET name = $3, updated_at = $4
|
||||
WHERE id = $1 AND address_book_id = $2
|
||||
RETURNING id, address_book_id, name, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(group.id)
|
||||
.bind(group.address_book_id)
|
||||
.bind(&group.name)
|
||||
.bind(group.updated_at)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
sqlx::Error::RowNotFound => DomainError::not_found("Contact group", group.id.to_string()),
|
||||
_ => DomainError::database_error(format!("Failed to update contact group: {}", e)),
|
||||
})?;
|
||||
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Por simplicidad, devolvemos el grupo original
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
// Begin transaction
|
||||
let mut tx = self.pool.begin().await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Delete group memberships
|
||||
sqlx::query(
|
||||
r#"DELETE FROM carddav.contact_group_members WHERE group_id = $1"#
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to delete group memberships: {}", e)))?;
|
||||
|
||||
// Delete the group
|
||||
sqlx::query(
|
||||
r#"DELETE FROM carddav.contact_groups WHERE id = $1"#
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to delete contact group: {}", e)))?;
|
||||
|
||||
// Commit transaction
|
||||
tx.commit().await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to commit transaction: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>> {
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT id, address_book_id, name, created_at, updated_at
|
||||
FROM carddav.contact_groups
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contact group: {}", e)))?;
|
||||
|
||||
if let Some(row) = row_opt {
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Para esta demostración, devolvemos un grupo predeterminado con el ID correcto
|
||||
let mut group = ContactGroup::default();
|
||||
group.id = id.clone();
|
||||
return Ok(Some(group));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT id, address_book_id, name, created_at, updated_at
|
||||
FROM carddav.contact_groups
|
||||
WHERE address_book_id = $1
|
||||
ORDER BY name
|
||||
"#
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contact groups by address book: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
|
||||
// Por simplicidad, devolvemos una lista vacía
|
||||
let groups = Vec::new();
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
// Check if the membership already exists
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT 1 FROM carddav.contact_group_members
|
||||
WHERE group_id = $1 AND contact_id = $2
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to check group membership: {}", e)))?;
|
||||
|
||||
let exists = row_opt.is_some();
|
||||
|
||||
if !exists {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.contact_group_members (group_id, contact_id)
|
||||
VALUES ($1, $2)
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to add contact to group: {}", e)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_contact_from_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM carddav.contact_group_members
|
||||
WHERE group_id = $1 AND contact_id = $2
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to remove contact from group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_contacts_in_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
// En lugar de implementar toda la lógica compleja que requiere query!, simplificamos
|
||||
// Devolvemos una lista vacía por simplicidad para evitar el uso de macros SQLx
|
||||
|
||||
// Para una implementación real, deberíamos convertir cada query! a sqlx::query
|
||||
// y manejar la conversión de resultados manualmente
|
||||
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
g.id, g.address_book_id, g.name, g.created_at, g.updated_at
|
||||
FROM carddav.contact_groups g
|
||||
JOIN carddav.contact_group_members m ON g.id = m.group_id
|
||||
WHERE m.contact_id = $1
|
||||
ORDER BY g.name
|
||||
"#
|
||||
)
|
||||
.bind(contact_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get groups for contact: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
|
||||
// Por simplicidad y demostración, devolvemos una lista vacía
|
||||
let groups = Vec::new();
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//! Persistence DTOs for Contact entities
|
||||
//!
|
||||
//! These DTOs are used for JSONB serialization/deserialization in PostgreSQL.
|
||||
//! They mirror the domain entities but include serde traits required for persistence.
|
||||
//! This keeps the domain layer free of infrastructure concerns (serde dependency).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::domain::entities::contact::{Email, Phone, Address};
|
||||
|
||||
/// Persistence DTO for Email - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EmailPersistenceDto {
|
||||
pub email: String,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Email> for EmailPersistenceDto {
|
||||
fn from(email: &Email) -> Self {
|
||||
Self {
|
||||
email: email.email.clone(),
|
||||
r#type: email.r#type.clone(),
|
||||
is_primary: email.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EmailPersistenceDto> for Email {
|
||||
fn from(dto: EmailPersistenceDto) -> Self {
|
||||
Self {
|
||||
email: dto.email,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistence DTO for Phone - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PhonePersistenceDto {
|
||||
pub number: String,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Phone> for PhonePersistenceDto {
|
||||
fn from(phone: &Phone) -> Self {
|
||||
Self {
|
||||
number: phone.number.clone(),
|
||||
r#type: phone.r#type.clone(),
|
||||
is_primary: phone.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PhonePersistenceDto> for Phone {
|
||||
fn from(dto: PhonePersistenceDto) -> Self {
|
||||
Self {
|
||||
number: dto.number,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistence DTO for Address - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddressPersistenceDto {
|
||||
pub street: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub postal_code: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Address> for AddressPersistenceDto {
|
||||
fn from(addr: &Address) -> Self {
|
||||
Self {
|
||||
street: addr.street.clone(),
|
||||
city: addr.city.clone(),
|
||||
state: addr.state.clone(),
|
||||
postal_code: addr.postal_code.clone(),
|
||||
country: addr.country.clone(),
|
||||
r#type: addr.r#type.clone(),
|
||||
is_primary: addr.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AddressPersistenceDto> for Address {
|
||||
fn from(dto: AddressPersistenceDto) -> Self {
|
||||
Self {
|
||||
street: dto.street,
|
||||
city: dto.city,
|
||||
state: dto.state,
|
||||
postal_code: dto.postal_code,
|
||||
country: dto.country,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper functions to convert collections
|
||||
pub fn emails_to_persistence(emails: &[Email]) -> Vec<EmailPersistenceDto> {
|
||||
emails.iter().map(EmailPersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn emails_from_persistence(dtos: Vec<EmailPersistenceDto>) -> Vec<Email> {
|
||||
dtos.into_iter().map(Email::from).collect()
|
||||
}
|
||||
|
||||
pub fn phones_to_persistence(phones: &[Phone]) -> Vec<PhonePersistenceDto> {
|
||||
phones.iter().map(PhonePersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn phones_from_persistence(dtos: Vec<PhonePersistenceDto>) -> Vec<Phone> {
|
||||
dtos.into_iter().map(Phone::from).collect()
|
||||
}
|
||||
|
||||
pub fn addresses_to_persistence(addresses: &[Address]) -> Vec<AddressPersistenceDto> {
|
||||
addresses.iter().map(AddressPersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn addresses_from_persistence(dtos: Vec<AddressPersistenceDto>) -> Vec<Address> {
|
||||
dtos.into_iter().map(Address::from).collect()
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use sqlx::{PgPool, query, query_as, types::Uuid};
|
||||
use sqlx::{PgPool, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::domain::entities::contact::{Contact, ContactGroup};
|
||||
use crate::domain::repositories::contact_repository::{ContactRepository, ContactGroupRepository, ContactRepositoryResult};
|
||||
use crate::common::errors::{DomainError, ErrorContext};
|
||||
use crate::domain::entities::contact::Contact;
|
||||
use crate::domain::repositories::contact_repository::{ContactRepository, ContactRepositoryResult};
|
||||
use crate::common::errors::DomainError;
|
||||
use super::contact_persistence_dto::{emails_to_persistence, phones_to_persistence, addresses_to_persistence};
|
||||
|
||||
pub struct ContactPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
@@ -21,12 +22,16 @@ impl ContactPgRepository {
|
||||
#[async_trait]
|
||||
impl ContactRepository for ContactPgRepository {
|
||||
async fn create_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact> {
|
||||
// Convert complex fields to JSON
|
||||
let email_json = serde_json::to_value(&contact.email).unwrap_or(JsonValue::Null);
|
||||
let phone_json = serde_json::to_value(&contact.phone).unwrap_or(JsonValue::Null);
|
||||
let address_json = serde_json::to_value(&contact.address).unwrap_or(JsonValue::Null);
|
||||
// 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 row = sqlx::query(
|
||||
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);
|
||||
let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null);
|
||||
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.contacts (
|
||||
id, address_book_id, uid, full_name, first_name, last_name, nickname,
|
||||
@@ -74,16 +79,20 @@ impl ContactRepository for ContactPgRepository {
|
||||
|
||||
async fn update_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact> {
|
||||
let now = Utc::now();
|
||||
// Convert complex fields to JSON
|
||||
let email_json = serde_json::to_value(&contact.email).unwrap_or(JsonValue::Null);
|
||||
let phone_json = serde_json::to_value(&contact.phone).unwrap_or(JsonValue::Null);
|
||||
let address_json = serde_json::to_value(&contact.address).unwrap_or(JsonValue::Null);
|
||||
// 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_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null);
|
||||
let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null);
|
||||
let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null);
|
||||
|
||||
// Create a clone of the contact with the updated timestamp
|
||||
let mut updated_contact = contact.clone();
|
||||
updated_contact.updated_at = now;
|
||||
|
||||
let row = sqlx::query(
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
UPDATE carddav.contacts
|
||||
SET
|
||||
@@ -312,205 +321,4 @@ impl ContactRepository for ContactPgRepository {
|
||||
|
||||
Ok(contacts)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContactGroupPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl ContactGroupPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ContactGroupRepository for ContactGroupPgRepository {
|
||||
async fn create_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, address_book_id, name, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(group.id)
|
||||
.bind(group.address_book_id)
|
||||
.bind(&group.name)
|
||||
.bind(group.created_at)
|
||||
.bind(group.updated_at)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to create contact group: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Por simplicidad, devolvemos el grupo original
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
let now = Utc::now();
|
||||
|
||||
// Create a clone of the group with updated timestamp
|
||||
let mut updated_group = group.clone();
|
||||
updated_group.updated_at = now;
|
||||
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
UPDATE carddav.contact_groups
|
||||
SET name = $1, updated_at = $2
|
||||
WHERE id = $3
|
||||
RETURNING id, address_book_id, name, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(&updated_group.name)
|
||||
.bind(now)
|
||||
.bind(updated_group.id)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to update contact group: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Por simplicidad, devolvemos el grupo con el timestamp actualizado
|
||||
Ok(updated_group)
|
||||
}
|
||||
|
||||
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM carddav.contact_groups
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to delete contact group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>> {
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT id, address_book_id, name, created_at, updated_at
|
||||
FROM carddav.contact_groups
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contact group by id: {}", e)))?;
|
||||
|
||||
if let Some(_row) = row_opt {
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Por simplicidad y demostración, devolvemos una instancia predeterminada
|
||||
return Ok(Some(ContactGroup::default()));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT id, address_book_id, name, created_at, updated_at
|
||||
FROM carddav.contact_groups
|
||||
WHERE address_book_id = $1
|
||||
ORDER BY name
|
||||
"#
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contact groups by address book: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
|
||||
// Por simplicidad y demostración, devolvemos una lista vacía
|
||||
let groups = Vec::new();
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.group_memberships (group_id, contact_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (group_id, contact_id) DO NOTHING
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to add contact to group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_contact_from_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM carddav.group_memberships
|
||||
WHERE group_id = $1 AND contact_id = $2
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to remove contact from group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_contacts_in_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
c.id, c.address_book_id, c.uid, c.full_name, c.first_name, c.last_name, c.nickname,
|
||||
c.email, c.phone, c.address, c.organization, c.title, c.notes, c.photo_url,
|
||||
c.birthday, c.anniversary, c.vcard, c.etag, c.created_at, c.updated_at
|
||||
FROM carddav.contacts c
|
||||
INNER JOIN carddav.group_memberships m ON c.id = m.contact_id
|
||||
WHERE m.group_id = $1
|
||||
ORDER BY c.full_name, c.first_name, c.last_name
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contacts in group: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos objetos Contact a partir de las filas
|
||||
// Por simplicidad y demostración, devolvemos una lista vacía
|
||||
let contacts = Vec::new();
|
||||
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
g.id, g.address_book_id, g.name, g.created_at, g.updated_at
|
||||
FROM carddav.contact_groups g
|
||||
INNER JOIN carddav.group_memberships m ON g.id = m.group_id
|
||||
WHERE m.contact_id = $1
|
||||
ORDER BY g.name
|
||||
"#
|
||||
)
|
||||
.bind(contact_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get groups for contact: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
|
||||
// Por simplicidad y demostración, devolvemos una lista vacía
|
||||
let groups = Vec::new();
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ mod address_book_pg_repository;
|
||||
mod calendar_pg_repository;
|
||||
mod calendar_event_pg_repository;
|
||||
mod contact_pg_repository;
|
||||
mod contact_group_pg_repository;
|
||||
mod contact_persistence_dto;
|
||||
mod session_pg_repository;
|
||||
mod transaction_utils;
|
||||
mod user_pg_repository;
|
||||
@@ -11,6 +11,6 @@ pub use address_book_pg_repository::AddressBookPgRepository;
|
||||
pub use calendar_pg_repository::CalendarPgRepository;
|
||||
pub use calendar_event_pg_repository::CalendarEventPgRepository;
|
||||
pub use contact_pg_repository::ContactPgRepository;
|
||||
pub use contact_group_pg_repository::ContactGroupPgRepository;
|
||||
pub use contact_persistence_dto::*;
|
||||
pub use session_pg_repository::SessionPgRepository;
|
||||
pub use user_pg_repository::UserPgRepository;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError, Executor};
|
||||
use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError};
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
@@ -50,81 +50,4 @@ where
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Variant that accepts a transaction isolation level
|
||||
pub async fn with_transaction_isolation<F, T, E>(
|
||||
pool: &Arc<PgPool>,
|
||||
operation_name: &str,
|
||||
isolation_level: TransactionIsolationLevel,
|
||||
operation: F,
|
||||
) -> Result<T, E>
|
||||
where
|
||||
F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result<T, E>>,
|
||||
E: From<SqlxError> + std::fmt::Display,
|
||||
{
|
||||
debug!("Starting database transaction with isolation level {:?} for: {}",
|
||||
isolation_level, operation_name);
|
||||
|
||||
// Begin transaction with specific isolation level
|
||||
let mut tx = pool.begin().await.map_err(|e| {
|
||||
error!("Failed to begin transaction for {}: {}", operation_name, e);
|
||||
E::from(e)
|
||||
})?;
|
||||
|
||||
// Set isolation level
|
||||
tx.execute(&format!("SET TRANSACTION ISOLATION LEVEL {}", isolation_level.to_string())[..])
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to set isolation level for {}: {}", operation_name, e);
|
||||
E::from(e)
|
||||
})?;
|
||||
|
||||
// Execute the operation within the transaction
|
||||
match operation(&mut tx).await {
|
||||
Ok(result) => {
|
||||
// If operation succeeds, commit the transaction
|
||||
match tx.commit().await {
|
||||
Ok(_) => {
|
||||
debug!("Transaction committed successfully for: {}", operation_name);
|
||||
Ok(result)
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to commit transaction for {}: {}", operation_name, e);
|
||||
Err(E::from(e))
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
// If operation fails, rollback the transaction
|
||||
if let Err(rollback_err) = tx.rollback().await {
|
||||
error!("Failed to rollback transaction for {}: {}", operation_name, rollback_err);
|
||||
// Still return the original error
|
||||
} else {
|
||||
info!("Transaction rolled back for {}: {}", operation_name, e);
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction isolation levels from SQL standard
|
||||
#[derive(Debug)]
|
||||
pub enum TransactionIsolationLevel {
|
||||
/// Read committed isolation level
|
||||
ReadCommitted,
|
||||
/// Repeatable read isolation level
|
||||
RepeatableRead,
|
||||
/// Serializable isolation level
|
||||
Serializable,
|
||||
}
|
||||
|
||||
impl ToString for TransactionIsolationLevel {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
TransactionIsolationLevel::ReadCommitted => "READ COMMITTED".to_string(),
|
||||
TransactionIsolationLevel::RepeatableRead => "REPEATABLE READ".to_string(),
|
||||
TransactionIsolationLevel::Serializable => "SERIALIZABLE".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,13 +29,12 @@ struct TrashedItemEntry {
|
||||
pub struct TrashFsRepository {
|
||||
trash_dir: PathBuf,
|
||||
trash_index_path: PathBuf,
|
||||
id_mapping_service: Arc<dyn IdMappingPort>,
|
||||
}
|
||||
|
||||
impl TrashFsRepository {
|
||||
pub fn new(
|
||||
storage_root: impl AsRef<Path>,
|
||||
id_mapping_service: Arc<dyn IdMappingPort>,
|
||||
_id_mapping_service: Arc<dyn IdMappingPort>,
|
||||
) -> Self {
|
||||
let trash_dir = storage_root.as_ref().join(".trash");
|
||||
let trash_index_path = trash_dir.join("trash_index.json");
|
||||
@@ -43,7 +42,6 @@ impl TrashFsRepository {
|
||||
Self {
|
||||
trash_dir,
|
||||
trash_index_path,
|
||||
id_mapping_service,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,14 +222,6 @@ impl TrashFsRepository {
|
||||
deletion_date: item.deletion_date.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtiene la ruta de un elemento en la papelera
|
||||
fn get_trash_path_for_item(&self, user_id: &Uuid, item_id: &Uuid) -> PathBuf {
|
||||
self.trash_dir
|
||||
.join("files")
|
||||
.join(user_id.to_string())
|
||||
.join(item_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -9,11 +9,9 @@ use tracing::debug;
|
||||
pub const DEFAULT_BUFFER_SIZE: usize = 64 * 1024; // 64KB
|
||||
|
||||
/// Número máximo por defecto de buffers en el pool
|
||||
#[allow(dead_code)]
|
||||
pub const DEFAULT_MAX_BUFFERS: usize = 100;
|
||||
|
||||
/// Tiempo de vida por defecto de un buffer inactivo (en segundos)
|
||||
#[allow(dead_code)]
|
||||
pub const DEFAULT_BUFFER_TTL: u64 = 60;
|
||||
|
||||
/// Buffer pooling para optimizar operaciones de lectura/escritura
|
||||
@@ -83,7 +81,6 @@ impl BufferPool {
|
||||
}
|
||||
|
||||
/// Crea un pool con configuración por defecto
|
||||
#[allow(dead_code)]
|
||||
pub fn default() -> Arc<Self> {
|
||||
Self::new(
|
||||
DEFAULT_BUFFER_SIZE,
|
||||
@@ -282,7 +279,6 @@ impl BorrowedBuffer {
|
||||
}
|
||||
|
||||
/// Obtiene una referencia a los datos utilizados
|
||||
#[allow(dead_code)]
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.buffer[..self.used_size]
|
||||
}
|
||||
@@ -302,7 +298,6 @@ impl BorrowedBuffer {
|
||||
}
|
||||
|
||||
/// Copia datos a este buffer y actualiza el tamaño usado
|
||||
#[allow(dead_code)]
|
||||
pub fn copy_from_slice(&mut self, data: &[u8]) -> usize {
|
||||
let copy_size = min(data.len(), self.buffer.len());
|
||||
self.buffer[..copy_size].copy_from_slice(&data[..copy_size]);
|
||||
@@ -311,7 +306,6 @@ impl BorrowedBuffer {
|
||||
}
|
||||
|
||||
/// Impide que el buffer se devuelva al pool al destruirse
|
||||
#[allow(dead_code)]
|
||||
pub fn do_not_return(mut self) -> Self {
|
||||
self.return_to_pool = false;
|
||||
self
|
||||
@@ -323,7 +317,6 @@ impl BorrowedBuffer {
|
||||
}
|
||||
|
||||
/// Obtiene el tamaño usado del buffer
|
||||
#[allow(dead_code)]
|
||||
pub fn used_size(&self) -> usize {
|
||||
self.used_size
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use tokio::sync::RwLock;
|
||||
|
||||
/// Representación de metadatos en caché
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CachedMetadata {
|
||||
/// Si el archivo o directorio existe
|
||||
pub exists: bool,
|
||||
@@ -23,7 +22,6 @@ pub struct CachedMetadata {
|
||||
}
|
||||
|
||||
/// Estructura para gestionar la caché de metadatos de archivos y directorios
|
||||
#[allow(dead_code)]
|
||||
pub struct StorageCacheManager {
|
||||
/// Caché de existencia y metadatos
|
||||
cache: RwLock<HashMap<PathBuf, CachedMetadata>>,
|
||||
@@ -37,7 +35,6 @@ pub struct StorageCacheManager {
|
||||
|
||||
impl StorageCacheManager {
|
||||
/// Crea una nueva instancia del gestor de caché
|
||||
#[allow(dead_code)]
|
||||
pub fn new(file_ttl_ms: u64, dir_ttl_ms: u64, max_entries: usize) -> Self {
|
||||
Self {
|
||||
cache: RwLock::new(HashMap::with_capacity(max_entries)),
|
||||
@@ -48,7 +45,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Crea una instancia por defecto del gestor de caché
|
||||
#[allow(dead_code)]
|
||||
pub fn default() -> Self {
|
||||
Self::new(
|
||||
60_000, // 1 minuto para archivos
|
||||
@@ -58,7 +54,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Verifica si un archivo o directorio existe en caché
|
||||
#[allow(dead_code)]
|
||||
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 {
|
||||
@@ -70,7 +65,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Obtiene los metadatos de un path desde la caché
|
||||
#[allow(dead_code)]
|
||||
async fn get_cached_metadata(&self, path: &PathBuf) -> Option<CachedMetadata> {
|
||||
let cache = self.cache.read().await;
|
||||
|
||||
@@ -85,7 +79,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Actualiza la caché con los metadatos de un path
|
||||
#[allow(dead_code)]
|
||||
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;
|
||||
@@ -115,7 +108,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Elimina entradas aleatorias de la caché cuando está llena
|
||||
#[allow(dead_code)]
|
||||
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();
|
||||
@@ -136,7 +128,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Inicia una tarea de limpieza periódica
|
||||
#[allow(dead_code)]
|
||||
pub fn start_cleanup_task(cache_manager: Arc<Self>) -> BoxFuture<'static, ()> {
|
||||
Box::pin(async move {
|
||||
let interval = Duration::from_secs(60); // Ejecutar cada minuto
|
||||
@@ -170,14 +161,12 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Invalida una entrada específica de la caché
|
||||
#[allow(dead_code)]
|
||||
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
|
||||
#[allow(dead_code)]
|
||||
pub async fn invalidate_folder(&self, folder_path: &PathBuf) {
|
||||
let mut cache = self.cache.write().await;
|
||||
|
||||
@@ -204,7 +193,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Obtiene el número actual de entradas en la caché
|
||||
#[allow(dead_code)]
|
||||
pub async fn cache_size(&self) -> usize {
|
||||
let cache = self.cache.read().await;
|
||||
cache.len()
|
||||
|
||||
@@ -48,14 +48,12 @@ pub trait CompressionService: Send + Sync {
|
||||
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>>;
|
||||
|
||||
/// Comprime un stream de datos
|
||||
#[allow(dead_code)]
|
||||
fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
|
||||
-> impl Stream<Item = io::Result<Bytes>> + Send
|
||||
where
|
||||
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
|
||||
|
||||
/// Descomprime un stream de datos
|
||||
#[allow(dead_code)]
|
||||
fn decompress_stream<S>(&self, compressed_stream: S)
|
||||
-> impl Stream<Item = io::Result<Bytes>> + Send
|
||||
where
|
||||
|
||||
@@ -47,14 +47,12 @@ pub struct FileMetadata {
|
||||
/// Ruta absoluta del archivo
|
||||
pub path: PathBuf,
|
||||
/// Si el archivo existe físicamente
|
||||
#[allow(dead_code)]
|
||||
pub exists: bool,
|
||||
/// Tipo de entrada (archivo, directorio)
|
||||
pub entry_type: CacheEntryType,
|
||||
/// Tamaño en bytes (para archivos)
|
||||
pub size: Option<u64>,
|
||||
/// Tipo MIME (para archivos)
|
||||
#[allow(dead_code)]
|
||||
pub mime_type: Option<String>,
|
||||
/// Timestamp de creación (UNIX epoch seconds)
|
||||
pub created_at: Option<u64>,
|
||||
@@ -259,7 +257,6 @@ impl FileMetadataCache {
|
||||
}
|
||||
|
||||
/// Verifica si un archivo existe
|
||||
#[allow(dead_code)]
|
||||
pub async fn exists(&self, path: &Path) -> Option<bool> {
|
||||
if let Some(metadata) = self.get_metadata(path).await {
|
||||
return Some(metadata.exists);
|
||||
@@ -269,7 +266,6 @@ impl FileMetadataCache {
|
||||
}
|
||||
|
||||
/// Verifica si un path es un directorio
|
||||
#[allow(dead_code)]
|
||||
pub async fn is_dir(&self, path: &Path) -> Option<bool> {
|
||||
if let Some(metadata) = self.get_metadata(path).await {
|
||||
return Some(metadata.entry_type == CacheEntryType::Directory);
|
||||
@@ -288,7 +284,6 @@ impl FileMetadataCache {
|
||||
}
|
||||
|
||||
/// Obtiene el tamaño de un archivo
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_size(&self, path: &Path) -> Option<u64> {
|
||||
if let Some(metadata) = self.get_metadata(path).await {
|
||||
return metadata.size;
|
||||
@@ -298,7 +293,6 @@ impl FileMetadataCache {
|
||||
}
|
||||
|
||||
/// Obtiene el tipo MIME de un archivo
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_mime_type(&self, path: &Path) -> Option<String> {
|
||||
if let Some(metadata) = self.get_metadata(path).await {
|
||||
return metadata.mime_type;
|
||||
|
||||
@@ -301,7 +301,6 @@ impl IdMappingOptimizer {
|
||||
}
|
||||
|
||||
/// Precargar un conjunto de rutas para obtener sus IDs en batch
|
||||
#[allow(dead_code)]
|
||||
pub async fn preload_paths(&self, paths: Vec<StoragePath>) -> Result<(), IdMappingError> {
|
||||
// Solo proceder si hay rutas para cargar
|
||||
if paths.is_empty() {
|
||||
@@ -342,7 +341,6 @@ impl IdMappingOptimizer {
|
||||
}
|
||||
|
||||
/// Precargar un conjunto de IDs para obtener sus rutas en batch
|
||||
#[allow(dead_code)]
|
||||
pub async fn preload_ids(&self, ids: Vec<String>) -> Result<(), IdMappingError> {
|
||||
// Solo proceder si hay IDs para cargar
|
||||
if ids.is_empty() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{RwLock, Mutex};
|
||||
use tokio::fs;
|
||||
use tokio::time;
|
||||
@@ -29,7 +28,6 @@ pub enum IdMappingError {
|
||||
SerializationError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Other error: {0}")]
|
||||
#[allow(dead_code)]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
@@ -69,9 +67,6 @@ struct IdMap {
|
||||
version: u32, // Versión para detectar cambios
|
||||
}
|
||||
|
||||
/// Constantes para configuración
|
||||
const SAVE_DEBOUNCE_MS: u64 = 0; // Sin debounce para garantizar guardado inmediato
|
||||
|
||||
/// Servicio para gestionar mapeos entre rutas y IDs únicos
|
||||
pub struct IdMappingService {
|
||||
map_path: PathBuf,
|
||||
@@ -490,7 +485,6 @@ impl IdMappingPort for IdMappingService {
|
||||
/// Synchronous helper for contexts where we can't use async
|
||||
impl IdMappingService {
|
||||
/// Create a new service synchronously (only for stubs and initialization)
|
||||
#[allow(dead_code)]
|
||||
pub fn new_sync(map_path: PathBuf) -> Self {
|
||||
// Create a minimal implementation for initialization purposes
|
||||
Self {
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
//! JWT-based token service implementation.
|
||||
//!
|
||||
//! This module provides JWT token generation and validation functionality,
|
||||
//! implementing the TokenServicePort trait defined in the application layer.
|
||||
|
||||
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey, Algorithm};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::application::ports::auth_ports::{TokenServicePort, TokenClaims};
|
||||
use crate::domain::entities::user::User;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Internal JWT claims structure for serialization.
|
||||
/// This is the actual JWT payload structure used by jsonwebtoken crate.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct JwtClaims {
|
||||
/// Subject identifier - contains the user ID
|
||||
pub sub: String,
|
||||
/// Expiration timestamp (seconds since Unix epoch)
|
||||
pub exp: i64,
|
||||
/// Issued at timestamp (seconds since Unix epoch)
|
||||
pub iat: i64,
|
||||
/// JWT unique ID for token tracking and revocation
|
||||
pub jti: String,
|
||||
/// Username for display and identification purposes
|
||||
pub username: String,
|
||||
/// User email for communication and identification
|
||||
pub email: String,
|
||||
/// User role for authorization checks
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
impl From<JwtClaims> for TokenClaims {
|
||||
fn from(claims: JwtClaims) -> Self {
|
||||
TokenClaims {
|
||||
sub: claims.sub,
|
||||
exp: claims.exp,
|
||||
iat: claims.iat,
|
||||
jti: claims.jti,
|
||||
username: claims.username,
|
||||
email: claims.email,
|
||||
role: claims.role,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JWT-based implementation of the TokenServicePort.
|
||||
///
|
||||
/// This service handles JWT token generation and validation for user authentication.
|
||||
/// It uses HS256 algorithm for signing tokens.
|
||||
pub struct JwtTokenService {
|
||||
/// Secret key used for signing JWT tokens
|
||||
jwt_secret: String,
|
||||
/// Expiration time for access tokens in seconds
|
||||
access_token_expiry: i64,
|
||||
/// Expiration time for refresh tokens in seconds
|
||||
refresh_token_expiry: i64,
|
||||
}
|
||||
|
||||
impl JwtTokenService {
|
||||
/// Create a new JwtTokenService with the specified configuration.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `jwt_secret` - Secret key for signing tokens (should be at least 32 bytes)
|
||||
/// * `access_token_expiry_secs` - Lifetime of access tokens in seconds
|
||||
/// * `refresh_token_expiry_secs` - Lifetime of refresh tokens in seconds
|
||||
pub fn new(jwt_secret: String, access_token_expiry_secs: i64, refresh_token_expiry_secs: i64) -> Self {
|
||||
Self {
|
||||
jwt_secret,
|
||||
access_token_expiry: access_token_expiry_secs,
|
||||
refresh_token_expiry: refresh_token_expiry_secs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenServicePort for JwtTokenService {
|
||||
fn generate_access_token(&self, user: &User) -> Result<String, DomainError> {
|
||||
let now = Utc::now().timestamp();
|
||||
|
||||
// Log information for debugging
|
||||
tracing::debug!(
|
||||
"Generating token for user: {}, id: {}, role: {}",
|
||||
user.username(),
|
||||
user.id(),
|
||||
user.role()
|
||||
);
|
||||
|
||||
let claims = JwtClaims {
|
||||
sub: user.id().to_string(),
|
||||
exp: now + self.access_token_expiry,
|
||||
iat: now,
|
||||
jti: Uuid::new_v4().to_string(),
|
||||
username: user.username().to_string(),
|
||||
email: user.email().to_string(),
|
||||
role: format!("{}", user.role()),
|
||||
};
|
||||
|
||||
// Log JWT claims for debugging
|
||||
tracing::debug!("JWT claims: sub={}, exp={}, iat={}", claims.sub, claims.exp, claims.iat);
|
||||
|
||||
encode(
|
||||
&Header::default(),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(self.jwt_secret.as_bytes())
|
||||
)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error generating token: {}", e);
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"TokenService",
|
||||
format!("Error al generar token: {}", e)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_token(&self, token: &str) -> Result<TokenClaims, DomainError> {
|
||||
let validation = Validation::new(Algorithm::HS256);
|
||||
|
||||
let token_data = decode::<JwtClaims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(self.jwt_secret.as_bytes()),
|
||||
&validation
|
||||
)
|
||||
.map_err(|e| {
|
||||
match e.kind() {
|
||||
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
|
||||
DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expirado")
|
||||
},
|
||||
_ => DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"TokenService",
|
||||
format!("Token inválido: {}", e)
|
||||
),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(token_data.claims.into())
|
||||
}
|
||||
|
||||
fn generate_refresh_token(&self) -> String {
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
fn refresh_token_expiry_secs(&self) -> i64 {
|
||||
self.refresh_token_expiry
|
||||
}
|
||||
|
||||
fn refresh_token_expiry_days(&self) -> i64 {
|
||||
self.refresh_token_expiry / (24 * 3600)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::domain::entities::user::{User, UserRole};
|
||||
|
||||
fn create_test_user() -> User {
|
||||
User::from_data(
|
||||
"test-user-id".to_string(),
|
||||
"testuser".to_string(),
|
||||
"test@example.com".to_string(),
|
||||
"hashed_password".to_string(),
|
||||
UserRole::User,
|
||||
1024 * 1024 * 1024, // 1GB
|
||||
0,
|
||||
chrono::Utc::now(),
|
||||
chrono::Utc::now(),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_and_validate_token() {
|
||||
let service = JwtTokenService::new(
|
||||
"test_secret_key_at_least_32_bytes_long".to_string(),
|
||||
3600, // 1 hour
|
||||
86400, // 1 day
|
||||
);
|
||||
|
||||
let user = create_test_user();
|
||||
let token = service.generate_access_token(&user).expect("Should generate token");
|
||||
|
||||
let claims = service.validate_token(&token).expect("Should validate token");
|
||||
assert_eq!(claims.sub, user.id());
|
||||
assert_eq!(claims.username, user.username());
|
||||
assert_eq!(claims.email, user.email());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_token_is_unique() {
|
||||
let service = JwtTokenService::new("secret".to_string(), 3600, 86400);
|
||||
|
||||
let token1 = service.generate_refresh_token();
|
||||
let token2 = service.generate_refresh_token();
|
||||
|
||||
assert_ne!(token1, token2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_token() {
|
||||
let service = JwtTokenService::new("secret".to_string(), 3600, 86400);
|
||||
|
||||
let result = service.validate_token("invalid_token");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,7 @@ pub mod file_metadata_cache;
|
||||
pub mod compression_service;
|
||||
pub mod buffer_pool;
|
||||
pub mod trash_cleanup_service;
|
||||
pub mod zip_service;
|
||||
pub mod zip_service;
|
||||
pub mod path_service;
|
||||
pub mod password_hasher;
|
||||
pub mod jwt_service;
|
||||
@@ -0,0 +1,91 @@
|
||||
//! Argon2-based password hasher implementation.
|
||||
//!
|
||||
//! This module provides a secure password hashing implementation using the Argon2id
|
||||
//! algorithm, which is the recommended choice for password hashing as of 2023+.
|
||||
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use argon2::password_hash::SaltString;
|
||||
use rand_core::OsRng;
|
||||
|
||||
use crate::application::ports::auth_ports::PasswordHasherPort;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Argon2-based implementation of the PasswordHasherPort.
|
||||
///
|
||||
/// Uses Argon2id algorithm which provides resistance against both side-channel
|
||||
/// and GPU-based attacks. This is the recommended algorithm for password hashing.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Argon2PasswordHasher {
|
||||
/// Argon2 hasher instance - uses default secure parameters
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl Argon2PasswordHasher {
|
||||
/// Create a new Argon2PasswordHasher with default secure parameters.
|
||||
pub fn new() -> Self {
|
||||
Self { _private: () }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Argon2PasswordHasher {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PasswordHasherPort for Argon2PasswordHasher {
|
||||
fn hash_password(&self, password: &str) -> Result<String, DomainError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
argon2.hash_password(password.as_bytes(), &salt)
|
||||
.map(|hash| hash.to_string())
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"PasswordHasher",
|
||||
format!("Error al generar hash de password: {}", e)
|
||||
))
|
||||
}
|
||||
|
||||
fn verify_password(&self, password: &str, hash: &str) -> Result<bool, DomainError> {
|
||||
let parsed_hash = PasswordHash::new(hash)
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"PasswordHasher",
|
||||
format!("Error al procesar hash: {}", e)
|
||||
))?;
|
||||
|
||||
Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hash_and_verify_password() {
|
||||
let hasher = Argon2PasswordHasher::new();
|
||||
let password = "test_password_123";
|
||||
|
||||
let hash = hasher.hash_password(password).expect("Should hash password");
|
||||
assert!(hasher.verify_password(password, &hash).expect("Should verify"));
|
||||
assert!(!hasher.verify_password("wrong_password", &hash).expect("Should verify"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_hashes_for_same_password() {
|
||||
let hasher = Argon2PasswordHasher::new();
|
||||
let password = "same_password";
|
||||
|
||||
let hash1 = hasher.hash_password(password).expect("Should hash");
|
||||
let hash2 = hasher.hash_password(password).expect("Should hash");
|
||||
|
||||
// Hashes should be different due to random salt
|
||||
assert_ne!(hash1, hash2);
|
||||
|
||||
// But both should verify correctly
|
||||
assert!(hasher.verify_password(password, &hash1).expect("Should verify"));
|
||||
assert!(hasher.verify_password(password, &hash2).expect("Should verify"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
//! PathService - Servicio de infraestructura para manejo de rutas de almacenamiento
|
||||
//!
|
||||
//! Este servicio fue movido desde domain/services porque implementa traits de application
|
||||
//! (StoragePort, StorageMediator) y tiene dependencias de sistema de archivos (tokio::fs).
|
||||
//!
|
||||
//! StoragePath (Value Object) permanece en domain/services/path_service.rs
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use async_trait::async_trait;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::application::ports::outbound::StoragePort;
|
||||
use crate::application::services::storage_mediator::{StorageMediator, StorageMediatorResult, StorageMediatorError};
|
||||
use crate::domain::entities::folder::Folder;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
|
||||
/// Servicio de infraestructura para manejar operaciones con rutas de almacenamiento
|
||||
pub struct PathService {
|
||||
root_path: PathBuf,
|
||||
}
|
||||
|
||||
impl PathService {
|
||||
/// Crea un nuevo servicio de rutas con una raíz específica
|
||||
pub fn new(root_path: PathBuf) -> Self {
|
||||
Self { root_path }
|
||||
}
|
||||
|
||||
/// Convierte una ruta del dominio a una ruta física absoluta
|
||||
pub fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
let mut path = self.root_path.clone();
|
||||
for segment in storage_path.segments() {
|
||||
path.push(segment);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
/// Convierte una ruta física a una ruta de dominio
|
||||
pub fn to_storage_path(&self, physical_path: &Path) -> Option<StoragePath> {
|
||||
physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| {
|
||||
let segments: Vec<String> = rel_path
|
||||
.components()
|
||||
.filter_map(|c| match c {
|
||||
std::path::Component::Normal(os_str) => Some(os_str.to_string_lossy().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
StoragePath::new(segments)
|
||||
})
|
||||
}
|
||||
|
||||
/// Crea una ruta de archivo dentro de una carpeta
|
||||
pub fn create_file_path(&self, folder_path: &StoragePath, file_name: &str) -> StoragePath {
|
||||
folder_path.join(file_name)
|
||||
}
|
||||
|
||||
/// Verifica si una ruta es directamente hija de otra
|
||||
pub fn is_direct_child(&self, parent_path: &StoragePath, potential_child: &StoragePath) -> bool {
|
||||
if let Some(child_parent) = potential_child.parent() {
|
||||
&child_parent == parent_path
|
||||
} else {
|
||||
parent_path.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifica si una ruta está en la raíz
|
||||
pub fn is_in_root(&self, path: &StoragePath) -> bool {
|
||||
path.parent().map_or(true, |p| p.is_empty())
|
||||
}
|
||||
|
||||
/// Gets the root path used by this service
|
||||
pub fn get_root_path(&self) -> &Path {
|
||||
&self.root_path
|
||||
}
|
||||
|
||||
/// Valida una ruta para asegurar que no contiene componentes peligrosos
|
||||
pub fn validate_path(&self, path: &StoragePath) -> Result<(), DomainError> {
|
||||
// Verificar que no haya segmentos vacíos
|
||||
if path.segments().iter().any(|s| s.is_empty()) {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path contains empty segments: {}", path.to_string())
|
||||
));
|
||||
}
|
||||
|
||||
// Verificar que no haya caracteres peligrosos
|
||||
let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
|
||||
for segment in path.segments() {
|
||||
if segment.contains(&dangerous_chars[..]) {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path contains dangerous characters: {}", segment)
|
||||
));
|
||||
}
|
||||
|
||||
// Verificar que no empiece con . (oculto en Unix)
|
||||
if segment.starts_with('.') && segment != ".well-known" {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path segments cannot start with dot: {}", segment)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StoragePort for PathService {
|
||||
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
let mut path = self.root_path.clone();
|
||||
for segment in storage_path.segments() {
|
||||
path.push(segment);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> {
|
||||
// Primero validar la ruta
|
||||
self.validate_path(storage_path)?;
|
||||
|
||||
// Resolver a ruta física
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
// Crear directorios si no existen
|
||||
if !physical_path.exists() {
|
||||
fs::create_dir_all(&physical_path).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Storage",
|
||||
format!("Failed to create directory: {}", physical_path.display())
|
||||
).with_source(e))?;
|
||||
|
||||
tracing::debug!("Created directory: {}", physical_path.display());
|
||||
} else if !physical_path.is_dir() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Storage",
|
||||
format!("Path exists but is not a directory: {}", physical_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
let exists = physical_path.exists() && physical_path.is_file();
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
let exists = physical_path.exists() && physical_path.is_dir();
|
||||
Ok(exists)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StorageMediator for PathService {
|
||||
async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf> {
|
||||
// This is a simplified implementation since PathService doesn't have direct
|
||||
// access to folder repository. It's typically used through a proxy.
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath> {
|
||||
// Simplified implementation - should be overridden by actual implementations
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder> {
|
||||
// Simplified implementation - should be overridden by actual implementations
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy()));
|
||||
Ok(abs_path.exists() && abs_path.is_file())
|
||||
}
|
||||
|
||||
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(storage_path);
|
||||
Ok(abs_path.exists() && abs_path.is_file())
|
||||
}
|
||||
|
||||
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy()));
|
||||
Ok(abs_path.exists() && abs_path.is_dir())
|
||||
}
|
||||
|
||||
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(storage_path);
|
||||
Ok(abs_path.exists() && abs_path.is_dir())
|
||||
}
|
||||
|
||||
fn resolve_path(&self, relative_path: &Path) -> PathBuf {
|
||||
// Convert path to storage path then resolve
|
||||
let path_str = relative_path.to_string_lossy().to_string();
|
||||
let storage_path = StoragePath::from_string(&path_str);
|
||||
PathService::resolve_path(self, &storage_path)
|
||||
}
|
||||
|
||||
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
PathService::resolve_path(self, storage_path)
|
||||
}
|
||||
|
||||
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
|
||||
let abs_path = PathService::resolve_path(self, &StoragePath::from_string(&path.to_string_lossy()));
|
||||
|
||||
if !abs_path.exists() {
|
||||
fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> {
|
||||
let abs_path = PathService::resolve_path(self, storage_path);
|
||||
|
||||
if !abs_path.exists() {
|
||||
fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let storage_path = StoragePath::from_string("test/file.txt");
|
||||
let absolute = service.resolve_path(&storage_path);
|
||||
|
||||
assert_eq!(absolute, PathBuf::from("/storage/test/file.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_storage_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let physical_path = PathBuf::from("/storage/folder/file.txt");
|
||||
let storage_path = service.to_storage_path(&physical_path).unwrap();
|
||||
|
||||
assert_eq!(storage_path.to_string(), "/folder/file.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_in_root() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let root_path = StoragePath::from_string("file.txt");
|
||||
let nested_path = StoragePath::from_string("folder/file.txt");
|
||||
|
||||
assert!(service.is_in_root(&root_path));
|
||||
assert!(!service.is_in_root(&nested_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_direct_child() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let parent = StoragePath::from_string("folder");
|
||||
let child = StoragePath::from_string("folder/file.txt");
|
||||
let not_child = StoragePath::from_string("folder2/file.txt");
|
||||
|
||||
assert!(service.is_direct_child(&parent, &child));
|
||||
assert!(!service.is_direct_child(&parent, ¬_child));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_file_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let folder_path = StoragePath::from_string("folder");
|
||||
let file_path = service.create_file_path(&folder_path, "file.txt");
|
||||
|
||||
assert_eq!(file_path.to_string(), "/folder/file.txt");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user