style: apply cargo fmt to entire codebase

Standardize code formatting across all 173 Rust source files
using rustfmt. No functional changes - purely cosmetic.

This establishes a consistent code style baseline for the
project going forward.
This commit is contained in:
Dionisio
2026-02-14 01:29:34 +01:00
parent 67137a3ef2
commit 4c98c5a657
173 changed files with 23368 additions and 17590 deletions
@@ -86,7 +86,9 @@ impl FileWritePort for CompositeFileRepository {
content_type: String,
content: Vec<u8>,
) -> Result<File, DomainError> {
self.write.save_file(name, folder_id, content_type, content).await
self.write
.save_file(name, folder_id, content_type, content)
.await
}
async fn save_file_from_stream(
@@ -96,7 +98,9 @@ impl FileWritePort for CompositeFileRepository {
content_type: String,
stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
) -> Result<File, DomainError> {
self.write.save_file_from_stream(name, folder_id, content_type, stream).await
self.write
.save_file_from_stream(name, folder_id, content_type, stream)
.await
}
async fn move_file(
@@ -107,11 +111,7 @@ impl FileWritePort for CompositeFileRepository {
self.write.move_file(file_id, target_folder_id).await
}
async fn rename_file(
&self,
file_id: &str,
new_name: &str,
) -> Result<File, DomainError> {
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<File, DomainError> {
self.write.rename_file(file_id, new_name).await
}
@@ -119,7 +119,11 @@ impl FileWritePort for CompositeFileRepository {
self.write.delete_file(id).await
}
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError> {
async fn update_file_content(
&self,
file_id: &str,
content: Vec<u8>,
) -> Result<(), DomainError> {
self.write.update_file_content(file_id, content).await
}
@@ -130,14 +134,20 @@ impl FileWritePort for CompositeFileRepository {
content_type: String,
size: u64,
) -> Result<(File, PathBuf), DomainError> {
self.write.register_file_deferred(name, folder_id, content_type, size).await
self.write
.register_file_deferred(name, folder_id, content_type, size)
.await
}
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError> {
self.write.move_to_trash(file_id).await
}
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError> {
async fn restore_from_trash(
&self,
file_id: &str,
original_path: &str,
) -> Result<(), DomainError> {
self.write.restore_from_trash(file_id, original_path).await
}
@@ -2,24 +2,26 @@ use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::{fs, time};
use tokio::fs::File as TokioFile;
use tokio_util::codec::{BytesCodec, FramedRead};
use futures::{Stream, StreamExt};
use bytes::Bytes;
use tokio::task;
use futures::{Stream, StreamExt};
use mime_guess::from_path;
use tokio::fs::File as TokioFile;
use tokio::task;
use tokio::{fs, time};
use tokio_util::codec::{BytesCodec, FramedRead};
use crate::domain::entities::file::File;
use crate::application::ports::storage_ports::FileReadPort;
use crate::common::errors::DomainError;
use crate::infrastructure::repositories::repository_errors::{FileRepositoryResult, FileRepositoryError};
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
use crate::application::ports::cache_ports::MetadataCachePort;
use crate::application::ports::storage_ports::FileReadPort;
use crate::application::services::storage_mediator::StorageMediator;
use crate::infrastructure::services::path_service::PathService;
use crate::domain::services::path_service::StoragePath;
use crate::common::config::AppConfig;
use crate::common::errors::DomainError;
use crate::domain::entities::file::File;
use crate::domain::services::path_service::StoragePath;
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
use crate::infrastructure::repositories::repository_errors::{
FileRepositoryError, FileRepositoryResult,
};
use crate::infrastructure::services::path_service::PathService;
/// Repository implementation for file **read** operations.
///
@@ -63,12 +65,13 @@ impl FileFsReadRepository {
Self {
root_path: PathBuf::from("./storage"),
storage_mediator: Arc::new(
crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub(),
crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub(
),
),
id_mapping_service: Arc::new(crate::common::stubs::StubIdMappingPort),
path_service: Arc::new(PathService::new(PathBuf::from("./storage"))),
metadata_cache: Arc::new(
crate::infrastructure::services::file_metadata_cache::FileMetadataCache::default()
crate::infrastructure::services::file_metadata_cache::FileMetadataCache::default(),
) as Arc<dyn MetadataCachePort>,
config: AppConfig::default(),
parallel_processor: None,
@@ -81,36 +84,61 @@ impl FileFsReadRepository {
self.path_service.resolve_path(storage_path)
}
async fn get_file_metadata_raw(&self, abs_path: &PathBuf) -> FileRepositoryResult<(u64, u64, u64)> {
async fn get_file_metadata_raw(
&self,
abs_path: &PathBuf,
) -> FileRepositoryResult<(u64, u64, u64)> {
// Cache first
if let Some(cached) = self.metadata_cache.get_metadata(abs_path).await
&& let (Some(s), Some(c), Some(m)) = (cached.size, cached.created_at, cached.modified_at) {
return Ok((s, c, m));
}
&& let (Some(s), Some(c), Some(m)) =
(cached.size, cached.created_at, cached.modified_at)
{
return Ok((s, c, m));
}
let metadata = time::timeout(self.config.timeouts.file_timeout(), fs::metadata(abs_path))
.await
.map_err(|_| FileRepositoryError::StorageError(format!("Timeout metadata: {}", abs_path.display())))?
.map_err(|_| {
FileRepositoryError::StorageError(format!(
"Timeout metadata: {}",
abs_path.display()
))
})?
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
let size = metadata.len();
let created_at = metadata.created()
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
let created_at = metadata
.created()
.map(|t| {
t.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
})
.unwrap_or(0);
let modified_at = metadata.modified()
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
let modified_at = metadata
.modified()
.map(|t| {
t.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
})
.unwrap_or(0);
let _ = self.metadata_cache.refresh_metadata(abs_path).await;
Ok((size, created_at, modified_at))
}
async fn get_file_by_id(&self, id: &str) -> FileRepositoryResult<File> {
let storage_path = self.id_mapping_service.get_path_by_id(id).await
let storage_path = self
.id_mapping_service
.get_path_by_id(id)
.await
.map_err(|e| FileRepositoryError::Other(e.to_string()))?;
let abs_path = self.resolve_storage_path(&storage_path);
if !abs_path.exists() || !abs_path.is_file() {
return Err(FileRepositoryError::NotFound(
format!("File {} not found at {}", id, storage_path.to_string()),
));
return Err(FileRepositoryError::NotFound(format!(
"File {} not found at {}",
id,
storage_path.to_string()
)));
}
let (size, created_at, modified_at) = self.get_file_metadata_raw(&abs_path).await?;
@@ -120,8 +148,14 @@ impl FileFsReadRepository {
let mime_type = from_path(&abs_path).first_or_octet_stream().to_string();
File::with_timestamps(
id.to_string(), name, storage_path, size, mime_type, None,
created_at, modified_at,
id.to_string(),
name,
storage_path,
size,
mime_type,
None,
created_at,
modified_at,
)
.map_err(|e| FileRepositoryError::Other(e.to_string()))
}
@@ -153,18 +187,14 @@ impl FileReadPort for FileFsReadRepository {
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError> {
let folder_storage_path = match folder_id {
Some(id) => {
match self.storage_mediator.get_folder_path(id).await {
Ok(path) => {
let lossy = path.to_string_lossy().to_string();
let folder_name = path.file_name()
.and_then(|f| f.to_str())
.unwrap_or(&lossy);
StoragePath::from_string(folder_name)
}
Err(_) => return Ok(Vec::new()),
Some(id) => match self.storage_mediator.get_folder_path(id).await {
Ok(path) => {
let lossy = path.to_string_lossy().to_string();
let folder_name = path.file_name().and_then(|f| f.to_str()).unwrap_or(&lossy);
StoragePath::from_string(folder_name)
}
}
Err(_) => return Ok(Vec::new()),
},
None => StoragePath::root(),
};
@@ -174,16 +204,24 @@ impl FileReadPort for FileFsReadRepository {
}
let mut files_result = Vec::new();
let mut entries = fs::read_dir(&abs_folder_path).await
let mut entries = fs::read_dir(&abs_folder_path)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
while let Some(entry) = entries.next_entry().await
while let Some(entry) = entries
.next_entry()
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?
{
let path = entry.path();
if !path.is_file() { continue; }
if !path.is_file() {
continue;
}
let file_name = entry.file_name().to_string_lossy().to_string();
if file_name.starts_with('.') || file_name == "folder_ids.json" || file_name == "file_ids.json" {
if file_name.starts_with('.')
|| file_name == "folder_ids.json"
|| file_name == "file_ids.json"
{
continue;
}
let metadata = match fs::metadata(&path).await {
@@ -191,20 +229,43 @@ impl FileReadPort for FileFsReadRepository {
Err(_) => continue,
};
let file_storage_path = folder_storage_path.join(&file_name);
let id = match self.id_mapping_service.get_or_create_id(&file_storage_path).await {
let id = match self
.id_mapping_service
.get_or_create_id(&file_storage_path)
.await
{
Ok(id) => id,
Err(_) => continue,
};
let size = metadata.len();
let created_at = metadata.created()
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
let created_at = metadata
.created()
.map(|t| {
t.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
})
.unwrap_or(0);
let modified_at = metadata.modified()
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
let modified_at = metadata
.modified()
.map(|t| {
t.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
})
.unwrap_or(0);
let mime_type = from_path(&path).first_or_octet_stream().to_string();
match File::with_timestamps(id, file_name, file_storage_path, size, mime_type, folder_id.map(String::from), created_at, modified_at) {
match File::with_timestamps(
id,
file_name,
file_storage_path,
size,
mime_type,
folder_id.map(String::from),
created_at,
modified_at,
) {
Ok(file) => files_result.push(file),
Err(_) => continue,
}
@@ -216,23 +277,39 @@ impl FileReadPort for FileFsReadRepository {
}
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError> {
let file = self.get_file_by_id(id).await
let file = self
.get_file_by_id(id)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let abs_path = self.resolve_storage_path(file.storage_path());
let metadata = time::timeout(self.config.timeouts.file_timeout(), fs::metadata(&abs_path))
.await
.map_err(|_| DomainError::internal_error("File", format!("Timeout metadata: {}", abs_path.display())))?
.map_err(|_| {
DomainError::internal_error(
"File",
format!("Timeout metadata: {}", abs_path.display()),
)
})?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let file_size = metadata.len();
if !self.config.resources.can_load_in_memory(file_size) {
return Err(DomainError::internal_error("File",
format!("File too large for memory: {} MB", file_size / (1024 * 1024))));
return Err(DomainError::internal_error(
"File",
format!(
"File too large for memory: {} MB",
file_size / (1024 * 1024)
),
));
}
// Parallel read for very large files
if self.config.resources.needs_parallel_processing(file_size, &self.config.concurrency) {
if self
.config
.resources
.needs_parallel_processing(file_size, &self.config.concurrency)
{
let content = if let Some(processor) = &self.parallel_processor {
processor.read_file_parallel(&abs_path).await
} else {
@@ -247,13 +324,14 @@ impl FileReadPort for FileFsReadRepository {
let abs_clone = abs_path.clone();
let chunk_size = self.config.resources.chunk_size_bytes;
let content = task::spawn_blocking(move || -> std::io::Result<Vec<u8>> {
use std::io::{Read, BufReader};
use std::io::{BufReader, Read};
let file = std::fs::File::open(&abs_clone)?;
let mut reader = BufReader::with_capacity(chunk_size, file);
let mut buf = Vec::with_capacity(file_size as usize);
reader.read_to_end(&mut buf)?;
Ok(buf)
}).await
})
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
return Ok(content);
@@ -262,7 +340,12 @@ impl FileReadPort for FileFsReadRepository {
// Small files — async read
time::timeout(self.config.timeouts.file_timeout(), fs::read(&abs_path))
.await
.map_err(|_| DomainError::internal_error("File", format!("Timeout reading: {}", abs_path.display())))?
.map_err(|_| {
DomainError::internal_error(
"File",
format!("Timeout reading: {}", abs_path.display()),
)
})?
.map_err(|e| DomainError::internal_error("File", e.to_string()))
}
@@ -270,7 +353,9 @@ impl FileReadPort for FileFsReadRepository {
&self,
id: &str,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
let file = self.get_file_by_id(id).await
let file = self
.get_file_by_id(id)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let abs_path = self.resolve_storage_path(file.storage_path());
@@ -281,15 +366,22 @@ impl FileReadPort for FileFsReadRepository {
let file_size = metadata.len();
let is_large = self.config.resources.is_large_file(file_size);
let fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::open(&abs_path))
.await
.map_err(|_| DomainError::internal_error("File", "Timeout opening file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let fh = time::timeout(
self.config.timeouts.file_timeout(),
TokioFile::open(&abs_path),
)
.await
.map_err(|_| DomainError::internal_error("File", "Timeout opening file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let chunk_size = if is_large { self.config.resources.chunk_size_bytes } else { 4096 };
let chunk_size = if is_large {
self.config.resources.chunk_size_bytes
} else {
4096
};
let codec = BytesCodec::new();
let stream = FramedRead::with_capacity(fh, codec, chunk_size)
.map(|r| r.map(|bm| bm.freeze()));
let stream =
FramedRead::with_capacity(fh, codec, chunk_size).map(|r| r.map(|bm| bm.freeze()));
Ok(Box::new(stream))
}
@@ -301,7 +393,9 @@ impl FileReadPort for FileFsReadRepository {
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
use tokio::io::AsyncSeekExt;
let file = self.get_file_by_id(id).await
let file = self
.get_file_by_id(id)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let abs_path = self.resolve_storage_path(file.storage_path());
@@ -311,31 +405,43 @@ impl FileReadPort for FileFsReadRepository {
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let file_size = metadata.len();
if start >= file_size {
return Err(DomainError::internal_error("File",
format!("Range start {} beyond file size {}", start, file_size)));
return Err(DomainError::internal_error(
"File",
format!("Range start {} beyond file size {}", start, file_size),
));
}
let actual_end = end.map(|e| e.min(file_size - 1)).unwrap_or(file_size - 1);
let range_length = actual_end - start + 1;
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::open(&abs_path))
let mut fh = time::timeout(
self.config.timeouts.file_timeout(),
TokioFile::open(&abs_path),
)
.await
.map_err(|_| DomainError::internal_error("File", "Timeout opening file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.seek(std::io::SeekFrom::Start(start))
.await
.map_err(|_| DomainError::internal_error("File", "Timeout opening file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.seek(std::io::SeekFrom::Start(start)).await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let chunk_size = if range_length > 1024 * 1024 { self.config.resources.chunk_size_bytes } else { 8192 };
let chunk_size = if range_length > 1024 * 1024 {
self.config.resources.chunk_size_bytes
} else {
8192
};
use tokio::io::AsyncReadExt;
let limited = fh.take(range_length);
let codec = BytesCodec::new();
let stream = FramedRead::with_capacity(limited, codec, chunk_size)
.map(|r| r.map(|bm| bm.freeze()));
let stream =
FramedRead::with_capacity(limited, codec, chunk_size).map(|r| r.map(|bm| bm.freeze()));
Ok(Box::new(stream))
}
async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError> {
use memmap2::Mmap;
let file = self.get_file_by_id(id).await
let file = self
.get_file_by_id(id)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let abs_path = self.resolve_storage_path(file.storage_path());
let path_clone = abs_path.clone();
@@ -346,7 +452,8 @@ impl FileReadPort for FileFsReadRepository {
let mmap = unsafe { Mmap::map(&fh) }
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
Ok(Bytes::copy_from_slice(&mmap[..]))
}).await
})
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?
}
@@ -363,4 +470,4 @@ impl FileReadPort for FileFsReadRepository {
_ => Ok("root".to_string()),
}
}
}
}
@@ -1,25 +1,27 @@
use async_trait::async_trait;
use bytes::Bytes;
use futures::{Stream, StreamExt};
use mime_guess::from_path;
use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::{fs, time};
use tokio::fs::File as TokioFile;
use tokio::io::AsyncWriteExt;
use futures::{Stream, StreamExt};
use bytes::Bytes;
use mime_guess::from_path;
use tokio::task;
use tokio::{fs, time};
use crate::domain::entities::file::File;
use crate::application::ports::storage_ports::FileWritePort;
use crate::common::errors::DomainError;
use crate::infrastructure::repositories::repository_errors::{FileRepositoryResult, FileRepositoryError};
use crate::infrastructure::services::file_system_utils::FileSystemUtils;
use crate::application::ports::cache_ports::MetadataCachePort;
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
use crate::application::ports::storage_ports::FileWritePort;
use crate::application::services::storage_mediator::StorageMediator;
use crate::infrastructure::services::path_service::PathService;
use crate::domain::services::path_service::StoragePath;
use crate::common::config::AppConfig;
use crate::common::errors::DomainError;
use crate::domain::entities::file::File;
use crate::domain::services::path_service::StoragePath;
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
use crate::infrastructure::repositories::repository_errors::{
FileRepositoryError, FileRepositoryResult,
};
use crate::infrastructure::services::file_system_utils::FileSystemUtils;
use crate::infrastructure::services::path_service::PathService;
/// Repository implementation for file **write** operations.
///
@@ -47,7 +49,15 @@ impl FileFsWriteRepository {
config: AppConfig,
parallel_processor: Option<Arc<ParallelFileProcessor>>,
) -> Self {
Self { root_path, storage_mediator, id_mapping_service, path_service, metadata_cache, config, parallel_processor }
Self {
root_path,
storage_mediator,
id_mapping_service,
path_service,
metadata_cache,
config,
parallel_processor,
}
}
/// Stub for testing (does not perform real I/O).
@@ -55,12 +65,13 @@ impl FileFsWriteRepository {
Self {
root_path: PathBuf::from("./storage"),
storage_mediator: Arc::new(
crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub(),
crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub(
),
),
id_mapping_service: Arc::new(crate::common::stubs::StubIdMappingPort),
path_service: Arc::new(PathService::new(PathBuf::from("./storage"))),
metadata_cache: Arc::new(
crate::infrastructure::services::file_metadata_cache::FileMetadataCache::default()
crate::infrastructure::services::file_metadata_cache::FileMetadataCache::default(),
) as Arc<dyn MetadataCachePort>,
config: AppConfig::default(),
parallel_processor: None,
@@ -78,14 +89,23 @@ impl FileFsWriteRepository {
time::timeout(
self.config.timeouts.dir_timeout(),
FileSystemUtils::create_dir_with_sync(parent),
).await
.map_err(|_| FileRepositoryError::StorageError(format!("Timeout creating dir: {}", parent.display())))?
)
.await
.map_err(|_| {
FileRepositoryError::StorageError(format!(
"Timeout creating dir: {}",
parent.display()
))
})?
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
}
Ok(())
}
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> FileRepositoryResult<bool> {
async fn file_exists_at_storage_path(
&self,
storage_path: &StoragePath,
) -> FileRepositoryResult<bool> {
let abs = self.resolve_storage_path(storage_path);
if let Some(is_file) = self.metadata_cache.is_file(&abs).await {
return Ok(is_file);
@@ -96,22 +116,46 @@ impl FileFsWriteRepository {
Ok(m.is_file())
}
Ok(Err(_)) => Ok(false),
Err(_) => Err(FileRepositoryError::StorageError(format!("Timeout: {}", abs.display()))),
Err(_) => Err(FileRepositoryError::StorageError(format!(
"Timeout: {}",
abs.display()
))),
}
}
async fn get_file_metadata_raw(&self, abs_path: &PathBuf) -> FileRepositoryResult<(u64, u64, u64)> {
async fn get_file_metadata_raw(
&self,
abs_path: &PathBuf,
) -> FileRepositoryResult<(u64, u64, u64)> {
if let Some(cached) = self.metadata_cache.get_metadata(abs_path).await
&& let (Some(s), Some(c), Some(m)) = (cached.size, cached.created_at, cached.modified_at) {
return Ok((s, c, m));
}
&& let (Some(s), Some(c), Some(m)) =
(cached.size, cached.created_at, cached.modified_at)
{
return Ok((s, c, m));
}
let meta = time::timeout(self.config.timeouts.file_timeout(), fs::metadata(abs_path))
.await
.map_err(|_| FileRepositoryError::StorageError(format!("Timeout: {}", abs_path.display())))?
.map_err(|_| {
FileRepositoryError::StorageError(format!("Timeout: {}", abs_path.display()))
})?
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
let s = meta.len();
let c = meta.created().map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()).unwrap_or(0);
let m = meta.modified().map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()).unwrap_or(0);
let c = meta
.created()
.map(|t| {
t.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
})
.unwrap_or(0);
let m = meta
.modified()
.map(|t| {
t.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
})
.unwrap_or(0);
let _ = self.metadata_cache.refresh_metadata(abs_path).await;
Ok((s, c, m))
}
@@ -159,14 +203,19 @@ impl FileFsWriteRepository {
Err(_) => 0,
};
if self.config.resources.is_large_file(file_size) {
task::spawn_blocking(move || { let _ = std::fs::remove_file(&abs_path); })
.await
.map_err(|e| FileRepositoryError::Other(e.to_string()))?;
task::spawn_blocking(move || {
let _ = std::fs::remove_file(&abs_path);
})
.await
.map_err(|e| FileRepositoryError::Other(e.to_string()))?;
} else {
time::timeout(self.config.timeouts.file_timeout(), fs::remove_file(&abs_path))
.await
.map_err(|_| FileRepositoryError::StorageError("Timeout deleting file".into()))?
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
time::timeout(
self.config.timeouts.file_timeout(),
fs::remove_file(&abs_path),
)
.await
.map_err(|_| FileRepositoryError::StorageError("Timeout deleting file".into()))?
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
}
Ok(())
}
@@ -177,11 +226,14 @@ impl FileFsWriteRepository {
match self.id_mapping_service.save_changes().await {
Ok(_) => {
if let Ok(verified) = self.id_mapping_service.get_path_by_id(id).await
&& verified.to_string() == expected_path {
return Ok(());
}
&& verified.to_string() == expected_path
{
return Ok(());
}
if attempt == 3 {
return Err(FileRepositoryError::Other("Failed to verify ID mapping after 3 attempts".into()));
return Err(FileRepositoryError::Other(
"Failed to verify ID mapping after 3 attempts".into(),
));
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
@@ -189,7 +241,12 @@ impl FileFsWriteRepository {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
tracing::warn!("ID mapping save retry {}: {}", attempt, e);
}
Err(e) => return Err(FileRepositoryError::Other(format!("Save ID mapping failed: {}", e))),
Err(e) => {
return Err(FileRepositoryError::Other(format!(
"Save ID mapping failed: {}",
e
)));
}
}
}
Ok(())
@@ -229,49 +286,97 @@ impl FileWritePort for FileFsWriteRepository {
content: Vec<u8>,
) -> Result<File, DomainError> {
let folder_path = self.resolve_folder_path(&folder_id).await;
let (file_storage_path, actual_name) = self.unique_file_path(&folder_path, &name).await.map_err(map_repo_err)?;
let (file_storage_path, actual_name) = self
.unique_file_path(&folder_path, &name)
.await
.map_err(map_repo_err)?;
let abs_path = self.resolve_storage_path(&file_storage_path);
self.ensure_parent_directory(&abs_path).await.map_err(map_repo_err)?;
self.ensure_parent_directory(&abs_path)
.await
.map_err(map_repo_err)?;
let content_size = content.len() as u64;
// Write strategy based on file size
if self.config.resources.needs_parallel_processing(content_size, &self.config.concurrency) {
if self
.config
.resources
.needs_parallel_processing(content_size, &self.config.concurrency)
{
if let Some(proc) = &self.parallel_processor {
proc.write_file_parallel(&abs_path, &content).await.map_err(map_repo_err)?;
proc.write_file_parallel(&abs_path, &content)
.await
.map_err(map_repo_err)?;
} else {
let proc = ParallelFileProcessor::new(self.config.clone());
proc.write_file_parallel(&abs_path, &content).await.map_err(map_repo_err)?;
proc.write_file_parallel(&abs_path, &content)
.await
.map_err(map_repo_err)?;
}
} else if content_size > self.config.resources.large_file_threshold_mb * 1024 * 1024 {
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::create(&abs_path))
.await
.map_err(|_| DomainError::internal_error("File", "Timeout creating file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let mut fh = time::timeout(
self.config.timeouts.file_timeout(),
TokioFile::create(&abs_path),
)
.await
.map_err(|_| DomainError::internal_error("File", "Timeout creating file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let chunk_size = self.config.resources.chunk_size_bytes;
for chunk in content.chunks(chunk_size) {
fh.write_all(chunk).await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.write_all(chunk)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
}
fh.flush().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
} else {
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::create(&abs_path))
fh.flush()
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
} else {
let mut fh = time::timeout(
self.config.timeouts.file_timeout(),
TokioFile::create(&abs_path),
)
.await
.map_err(|_| DomainError::internal_error("File", "Timeout creating file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.write_all(&content)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.flush()
.await
.map_err(|_| DomainError::internal_error("File", "Timeout creating file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.write_all(&content).await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.flush().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
}
let (size, created_at, modified_at) = self.get_file_metadata_raw(&abs_path).await.map_err(map_repo_err)?;
let mime = if content_type.is_empty() { from_path(&abs_path).first_or_octet_stream().to_string() } else { content_type };
let id = self.id_mapping_service.get_or_create_id(&file_storage_path).await
let (size, created_at, modified_at) = self
.get_file_metadata_raw(&abs_path)
.await
.map_err(map_repo_err)?;
let mime = if content_type.is_empty() {
from_path(&abs_path).first_or_octet_stream().to_string()
} else {
content_type
};
let id = self
.id_mapping_service
.get_or_create_id(&file_storage_path)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let path_string = file_storage_path.to_string();
let file = File::with_timestamps(id.clone(), actual_name, file_storage_path, size, mime, folder_id, created_at, modified_at)
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let file = File::with_timestamps(
id.clone(),
actual_name,
file_storage_path,
size,
mime,
folder_id,
created_at,
modified_at,
)
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
self.persist_id_mapping(&id, &path_string).await.map_err(map_repo_err)?;
self.persist_id_mapping(&id, &path_string)
.await
.map_err(map_repo_err)?;
if let Some(parent) = abs_path.parent() {
self.metadata_cache.invalidate_directory(parent).await;
}
@@ -286,45 +391,86 @@ impl FileWritePort for FileFsWriteRepository {
mut stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
) -> Result<File, DomainError> {
let folder_path = self.resolve_folder_path(&folder_id).await;
let (file_storage_path, actual_name) = self.unique_file_path(&folder_path, &name).await.map_err(map_repo_err)?;
let (file_storage_path, actual_name) = self
.unique_file_path(&folder_path, &name)
.await
.map_err(map_repo_err)?;
let abs_path = self.resolve_storage_path(&file_storage_path);
self.ensure_parent_directory(&abs_path).await.map_err(map_repo_err)?;
self.ensure_parent_directory(&abs_path)
.await
.map_err(map_repo_err)?;
let temp_path = abs_path.with_extension("tmp.upload");
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::create(&temp_path))
.await
.map_err(|_| DomainError::internal_error("File", "Timeout creating temp file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let mut fh = time::timeout(
self.config.timeouts.file_timeout(),
TokioFile::create(&temp_path),
)
.await
.map_err(|_| DomainError::internal_error("File", "Timeout creating temp file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let mut total_bytes: u64 = 0;
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.write_all(&chunk).await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let chunk =
chunk_result.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.write_all(&chunk)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
total_bytes += chunk.len() as u64;
}
fh.flush().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.sync_all().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.flush()
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
fh.sync_all()
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
drop(fh);
// Atomic rename
fs::rename(&temp_path, &abs_path).await
fs::rename(&temp_path, &abs_path)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let (size, created_at, modified_at) = self.get_file_metadata_raw(&abs_path).await.map_err(map_repo_err)?;
let mime = if content_type.is_empty() { from_path(&abs_path).first_or_octet_stream().to_string() } else { content_type };
let id = self.id_mapping_service.get_or_create_id(&file_storage_path).await
let (size, created_at, modified_at) = self
.get_file_metadata_raw(&abs_path)
.await
.map_err(map_repo_err)?;
let mime = if content_type.is_empty() {
from_path(&abs_path).first_or_octet_stream().to_string()
} else {
content_type
};
let id = self
.id_mapping_service
.get_or_create_id(&file_storage_path)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let path_string = file_storage_path.to_string();
let log_name = actual_name.clone();
let file = File::with_timestamps(id.clone(), actual_name, file_storage_path, size, mime, folder_id, created_at, modified_at)
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let file = File::with_timestamps(
id.clone(),
actual_name,
file_storage_path,
size,
mime,
folder_id,
created_at,
modified_at,
)
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
self.persist_id_mapping(&id, &path_string).await.map_err(map_repo_err)?;
self.persist_id_mapping(&id, &path_string)
.await
.map_err(map_repo_err)?;
if let Some(parent) = abs_path.parent() {
self.metadata_cache.invalidate_directory(parent).await;
}
tracing::info!("✅ STREAMING UPLOAD COMPLETE: {} ({} bytes)", log_name, total_bytes);
tracing::info!(
"✅ STREAMING UPLOAD COMPLETE: {} ({} bytes)",
log_name,
total_bytes
);
Ok(file)
}
@@ -339,57 +485,87 @@ impl FileWritePort for FileFsWriteRepository {
if !old_abs.exists() || !old_abs.is_file() {
return Err(DomainError::not_found("File", file_id.to_string()));
}
let (size, created_at, modified_at) = self.get_file_metadata_raw(&old_abs).await.map_err(map_repo_err)?;
let name = original_path.file_name()
let (size, created_at, modified_at) = self
.get_file_metadata_raw(&old_abs)
.await
.map_err(map_repo_err)?;
let name = original_path
.file_name()
.ok_or_else(|| DomainError::internal_error("File", "Invalid path"))?;
let mime = from_path(&old_abs).first_or_octet_stream().to_string();
// Build target path
let target_folder_path = self.resolve_folder_path(&target_folder_id).await;
let new_storage_path = target_folder_path.join(&name);
if self.file_exists_at_storage_path(&new_storage_path).await.map_err(map_repo_err)? {
return Err(DomainError::already_exists("File",
format!("File already exists at {}", new_storage_path.to_string())));
if self
.file_exists_at_storage_path(&new_storage_path)
.await
.map_err(map_repo_err)?
{
return Err(DomainError::already_exists(
"File",
format!("File already exists at {}", new_storage_path.to_string()),
));
}
let new_abs = self.resolve_storage_path(&new_storage_path);
self.ensure_parent_directory(&new_abs).await.map_err(map_repo_err)?;
self.ensure_parent_directory(&new_abs)
.await
.map_err(map_repo_err)?;
// Rename
time::timeout(
self.config.timeouts.file_timeout(),
FileSystemUtils::rename_with_sync(&old_abs, &new_abs),
).await
)
.await
.map_err(|_| DomainError::internal_error("File", "Timeout moving file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
// Update mapping
self.id_mapping_service.update_path(file_id, &new_storage_path).await?;
self.id_mapping_service
.update_path(file_id, &new_storage_path)
.await?;
let _ = self.id_mapping_service.save_changes().await;
File::with_timestamps(file_id.to_string(), name, new_storage_path, size, mime, target_folder_id, created_at, modified_at)
.map_err(|e| DomainError::internal_error("File", e.to_string()))
File::with_timestamps(
file_id.to_string(),
name,
new_storage_path,
size,
mime,
target_folder_id,
created_at,
modified_at,
)
.map_err(|e| DomainError::internal_error("File", e.to_string()))
}
async fn rename_file(
&self,
file_id: &str,
new_name: &str,
) -> Result<File, DomainError> {
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<File, DomainError> {
// 1. Get current file info
let original_path = self.id_mapping_service.get_path_by_id(file_id).await?;
let old_abs = self.resolve_storage_path(&original_path);
if !old_abs.exists() || !old_abs.is_file() {
return Err(DomainError::not_found("File", file_id.to_string()));
}
let (size, created_at, modified_at) = self.get_file_metadata_raw(&old_abs).await.map_err(map_repo_err)?;
let (size, created_at, modified_at) = self
.get_file_metadata_raw(&old_abs)
.await
.map_err(map_repo_err)?;
// 2. Build new path (same parent directory, different filename)
let parent = original_path.parent()
let parent = original_path
.parent()
.unwrap_or_else(|| StoragePath::new(vec![]));
let new_storage_path = parent.join(new_name);
if self.file_exists_at_storage_path(&new_storage_path).await.map_err(map_repo_err)? {
return Err(DomainError::already_exists("File",
format!("File already exists: {}", new_name)));
if self
.file_exists_at_storage_path(&new_storage_path)
.await
.map_err(map_repo_err)?
{
return Err(DomainError::already_exists(
"File",
format!("File already exists: {}", new_name),
));
}
let new_abs = self.resolve_storage_path(&new_storage_path);
let mime = from_path(&new_abs).first_or_octet_stream().to_string();
@@ -398,12 +574,15 @@ impl FileWritePort for FileFsWriteRepository {
time::timeout(
self.config.timeouts.file_timeout(),
FileSystemUtils::rename_with_sync(&old_abs, &new_abs),
).await
)
.await
.map_err(|_| DomainError::internal_error("File", "Timeout renaming file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
// 4. Update id→path mapping
self.id_mapping_service.update_path(file_id, &new_storage_path).await?;
self.id_mapping_service
.update_path(file_id, &new_storage_path)
.await?;
let _ = self.id_mapping_service.save_changes().await;
File::with_timestamps(
@@ -428,7 +607,9 @@ impl FileWritePort for FileFsWriteRepository {
self.metadata_cache.invalidate_directory(parent).await;
}
self.delete_file_non_blocking(abs_path).await.map_err(map_repo_err)?;
self.delete_file_non_blocking(abs_path)
.await
.map_err(map_repo_err)?;
// Clean up the ID mapping so we don't leave orphaned entries
if let Err(e) = self.id_mapping_service.remove_id(id).await {
@@ -439,7 +620,11 @@ impl FileWritePort for FileFsWriteRepository {
Ok(())
}
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError> {
async fn update_file_content(
&self,
file_id: &str,
content: Vec<u8>,
) -> Result<(), DomainError> {
let storage_path = self.id_mapping_service.get_path_by_id(file_id).await?;
let physical_path = self.resolve_storage_path(&storage_path);
@@ -460,9 +645,14 @@ impl FileWritePort for FileFsWriteRepository {
size: u64,
) -> Result<(File, PathBuf), DomainError> {
let folder_path = self.resolve_folder_path(&folder_id).await;
let (file_storage_path, actual_name) = self.unique_file_path(&folder_path, &name).await.map_err(map_repo_err)?;
let (file_storage_path, actual_name) = self
.unique_file_path(&folder_path, &name)
.await
.map_err(map_repo_err)?;
let abs_path = self.resolve_storage_path(&file_storage_path);
self.ensure_parent_directory(&abs_path).await.map_err(map_repo_err)?;
self.ensure_parent_directory(&abs_path)
.await
.map_err(map_repo_err)?;
let mime = if content_type.is_empty() {
from_path(&abs_path).first_or_octet_stream().to_string()
@@ -474,12 +664,24 @@ impl FileWritePort for FileFsWriteRepository {
.unwrap_or_default()
.as_secs();
let id = self.id_mapping_service.get_or_create_id(&file_storage_path).await
let id = self
.id_mapping_service
.get_or_create_id(&file_storage_path)
.await
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let _ = self.id_mapping_service.save_changes().await;
let file = File::with_timestamps(id.clone(), actual_name, file_storage_path, size, mime, folder_id, now, now)
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
let file = File::with_timestamps(
id.clone(),
actual_name,
file_storage_path,
size,
mime,
folder_id,
now,
now,
)
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
tracing::debug!("⚡ Registered deferred file: {} -> {:?}", id, abs_path);
Ok((file, abs_path))
@@ -496,17 +698,21 @@ impl FileWritePort for FileFsWriteRepository {
// Create trash directory
let trash_dir = self.root_path.join(".trash").join("files");
fs::create_dir_all(&trash_dir).await
.map_err(|e| DomainError::internal_error("File", format!("Failed to create trash dir: {}", e)))?;
fs::create_dir_all(&trash_dir).await.map_err(|e| {
DomainError::internal_error("File", format!("Failed to create trash dir: {}", e))
})?;
// Move file to trash
let trash_path = trash_dir.join(file_id);
fs::rename(&abs_path, &trash_path).await
.map_err(|e| DomainError::internal_error("File", format!("Failed to move file to trash: {}", e)))?;
fs::rename(&abs_path, &trash_path).await.map_err(|e| {
DomainError::internal_error("File", format!("Failed to move file to trash: {}", e))
})?;
// Update mapping to trash location
let trash_storage_path = StoragePath::from_string(&format!(".trash/files/{}", file_id));
self.id_mapping_service.update_path(file_id, &trash_storage_path).await?;
self.id_mapping_service
.update_path(file_id, &trash_storage_path)
.await?;
let _ = self.id_mapping_service.save_changes().await;
// Invalidate cache
@@ -515,36 +721,57 @@ impl FileWritePort for FileFsWriteRepository {
self.metadata_cache.invalidate_directory(parent).await;
}
tracing::debug!("File moved to trash: {} -> {}", file_id, trash_path.display());
tracing::debug!(
"File moved to trash: {} -> {}",
file_id,
trash_path.display()
);
Ok(())
}
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError> {
async fn restore_from_trash(
&self,
file_id: &str,
original_path: &str,
) -> Result<(), DomainError> {
// Get current path (should be in trash)
let current_storage_path = self.id_mapping_service.get_path_by_id(file_id).await?;
let current_abs_path = self.resolve_storage_path(&current_storage_path);
if !current_abs_path.exists() {
return Err(DomainError::not_found("File", format!("File {} not found in trash", file_id)));
return Err(DomainError::not_found(
"File",
format!("File {} not found in trash", file_id),
));
}
// Ensure parent directory exists for original location
let original_storage_path = StoragePath::from_string(original_path);
let original_abs_path = self.resolve_storage_path(&original_storage_path);
if let Some(parent) = original_abs_path.parent() {
fs::create_dir_all(parent).await
.map_err(|e| DomainError::internal_error("File", format!("Failed to create parent dir: {}", e)))?;
fs::create_dir_all(parent).await.map_err(|e| {
DomainError::internal_error("File", format!("Failed to create parent dir: {}", e))
})?;
}
// Move file back to original location
fs::rename(&current_abs_path, &original_abs_path).await
.map_err(|e| DomainError::internal_error("File", format!("Failed to restore file: {}", e)))?;
fs::rename(&current_abs_path, &original_abs_path)
.await
.map_err(|e| {
DomainError::internal_error("File", format!("Failed to restore file: {}", e))
})?;
// Update mapping back to original path
self.id_mapping_service.update_path(file_id, &original_storage_path).await?;
self.id_mapping_service
.update_path(file_id, &original_storage_path)
.await?;
let _ = self.id_mapping_service.save_changes().await;
tracing::debug!("File restored from trash: {} -> {}", file_id, original_abs_path.display());
tracing::debug!(
"File restored from trash: {} -> {}",
file_id,
original_abs_path.display()
);
Ok(())
}
@@ -555,7 +782,9 @@ impl FileWritePort for FileFsWriteRepository {
// Delete the physical file if it exists
if abs_path.exists() {
self.delete_file_non_blocking(abs_path.clone()).await.map_err(map_repo_err)?;
self.delete_file_non_blocking(abs_path.clone())
.await
.map_err(map_repo_err)?;
}
// Remove ID mapping
@@ -568,4 +797,4 @@ impl FileWritePort for FileFsWriteRepository {
tracing::debug!("File permanently deleted: {}", file_id);
Ok(())
}
}
}
File diff suppressed because it is too large Load Diff
@@ -2,8 +2,8 @@ use std::path::PathBuf;
use tokio::fs;
use tracing::{debug, error};
use crate::infrastructure::repositories::repository_errors::FolderRepositoryResult;
use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository;
use crate::infrastructure::repositories::repository_errors::FolderRepositoryResult;
// This file contains the implementation of trash-related methods
// for the FolderFsRepository folder repository
@@ -14,17 +14,18 @@ impl FolderFsRepository {
fn get_trash_dir(&self) -> PathBuf {
self.get_root_path().join(".trash").join("folders")
}
// Creates a unique path in the trash for the folder
async fn create_trash_folder_path(&self, folder_id: &str) -> FolderRepositoryResult<PathBuf> {
let trash_dir = self.get_trash_dir();
// Ensure the trash directory exists
if !trash_dir.exists() {
fs::create_dir_all(&trash_dir).await
fs::create_dir_all(&trash_dir)
.await
.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?;
}
// Create a unique path for the folder in the trash
Ok(trash_dir.join(folder_id))
}
@@ -37,7 +38,7 @@ impl FolderFsRepository {
/// Helper method that will be used for trash functionality
pub(crate) async fn _trash_move_to_trash(&self, folder_id: &str) -> FolderRepositoryResult<()> {
debug!("Moving folder to trash: {}", folder_id);
// Get the physical path of the folder
let folder_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => path,
@@ -46,41 +47,55 @@ impl FolderFsRepository {
return Err(e);
}
};
let folder_path_buf = PathBuf::from(folder_path.to_string());
// Verify the folder exists
if !folder_path_buf.exists() {
return Err(FolderRepositoryError::NotFound(format!("Folder not found: {}", folder_id)));
return Err(FolderRepositoryError::NotFound(format!(
"Folder not found: {}",
folder_id
)));
}
// Create directory in the trash
let trash_folder_path = self.create_trash_folder_path(folder_id).await?;
// Physically move the folder to the trash
match fs::rename(&folder_path_buf, &trash_folder_path).await {
Ok(_) => {
debug!("Folder moved to trash: {} -> {}", folder_path_buf.display(), trash_folder_path.display());
debug!(
"Folder moved to trash: {} -> {}",
folder_path_buf.display(),
trash_folder_path.display()
);
// Update the mapping to the new path in the trash
if let Err(e) = self.update_mapped_folder_path(folder_id, &trash_folder_path).await {
if let Err(e) = self
.update_mapped_folder_path(folder_id, &trash_folder_path)
.await
{
error!("Error updating folder mapping in trash: {}", e);
return Err(e);
}
Ok(())
},
}
Err(e) => {
error!("Error moving folder to trash: {}", e);
Err(FolderRepositoryError::StorageError(e.to_string()))
}
}
}
/// Restores a folder from the trash to its original location
pub(crate) async fn _trash_restore_from_trash(&self, folder_id: &str, original_path: &str) -> FolderRepositoryResult<()> {
pub(crate) async fn _trash_restore_from_trash(
&self,
folder_id: &str,
original_path: &str,
) -> FolderRepositoryResult<()> {
debug!("Restoring folder {} to {}", folder_id, original_path);
// Get the current path in the trash
let current_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => PathBuf::from(path),
@@ -89,44 +104,54 @@ impl FolderFsRepository {
return Err(e);
}
};
// Convert the original path to PathBuf
let original_path_buf = PathBuf::from(original_path);
// Ensure the destination parent directory exists
if let Some(parent) = original_path_buf.parent()
&& !parent.exists() {
fs::create_dir_all(parent).await
.map_err(|e| {
error!("Error creating parent directory for restoration: {}", e);
FolderRepositoryError::StorageError(e.to_string())
})?;
}
&& !parent.exists()
{
fs::create_dir_all(parent).await.map_err(|e| {
error!("Error creating parent directory for restoration: {}", e);
FolderRepositoryError::StorageError(e.to_string())
})?;
}
// Move the folder from the trash to its original location
match fs::rename(&current_path, &original_path_buf).await {
Ok(_) => {
debug!("Folder restored: {} -> {}", current_path.display(), original_path_buf.display());
debug!(
"Folder restored: {} -> {}",
current_path.display(),
original_path_buf.display()
);
// Update the mapping to the original path
if let Err(e) = self.update_mapped_folder_path(folder_id, &original_path_buf).await {
if let Err(e) = self
.update_mapped_folder_path(folder_id, &original_path_buf)
.await
{
error!("Error updating restored folder mapping: {}", e);
return Err(e);
}
Ok(())
},
}
Err(e) => {
error!("Error restoring folder: {}", e);
Err(FolderRepositoryError::StorageError(e.to_string()))
}
}
}
/// Permanently deletes a folder (used by the trash)
pub(crate) async fn _trash_delete_folder_permanently(&self, folder_id: &str) -> FolderRepositoryResult<()> {
pub(crate) async fn _trash_delete_folder_permanently(
&self,
folder_id: &str,
) -> FolderRepositoryResult<()> {
debug!("Permanently deleting folder: {}", folder_id);
// Similar to delete_folder but without additional validations
let folder_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => PathBuf::from(path),
@@ -135,13 +160,13 @@ impl FolderFsRepository {
return Err(e);
}
};
// Delete the folder recursively
if folder_path.exists() {
match fs::remove_dir_all(&folder_path).await {
Ok(_) => {
debug!("Folder permanently deleted: {}", folder_path.display());
},
}
Err(e) => {
error!("Error permanently deleting folder: {}", e);
// Don't report error if the folder no longer exists
@@ -151,17 +176,17 @@ impl FolderFsRepository {
}
}
}
// Remove the mapping
if let Err(e) = self.remove_mapped_folder_id(folder_id).await {
error!("Error removing folder mapping: {}", e);
return Err(e);
}
debug!("Folder permanently deleted successfully: {}", folder_id);
Ok(())
}
}
// Re-exports needed by the compiler
use crate::infrastructure::repositories::repository_errors::FolderRepositoryError;
use crate::infrastructure::repositories::repository_errors::FolderRepositoryError;
+4 -4
View File
@@ -3,19 +3,19 @@ pub mod parallel_file_processor;
pub mod repository_errors;
// Repositorios CQRS (Read/Write) + composite
pub mod composite_file_repository;
pub mod file_fs_read_repository;
pub mod file_fs_write_repository;
pub mod composite_file_repository;
pub mod trash_fs_repository;
pub mod folder_fs_repository_trash;
pub mod share_fs_repository;
pub mod trash_fs_repository;
// Repositorios PostgreSQL
pub mod pg;
// Re-exportar para facilitar acceso
pub use composite_file_repository::CompositeFileRepository;
pub use file_fs_read_repository::FileFsReadRepository;
pub use file_fs_write_repository::FileFsWriteRepository;
pub use composite_file_repository::CompositeFileRepository;
pub use pg::{UserPgRepository, SessionPgRepository};
pub use pg::{SessionPgRepository, UserPgRepository};
@@ -1,13 +1,13 @@
use bytes::{Bytes, BytesMut};
use futures::future::join_all;
use std::io::{self, SeekFrom};
use std::path::PathBuf;
use std::sync::Arc;
use std::io::{self, SeekFrom};
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
use tokio::sync::{Mutex, Semaphore};
use tokio::task;
use tokio::sync::{Semaphore, Mutex};
use futures::future::join_all;
use tracing::{info, debug, error};
use bytes::{Bytes, BytesMut};
use tracing::{debug, error, info};
use crate::common::config::AppConfig;
use crate::infrastructure::repositories::repository_errors::FileRepositoryError;
@@ -39,11 +39,11 @@ impl BytesBufferPool {
max_buffers,
}
}
/// Get a buffer from the pool or create a new one
pub async fn get_buffer(&self) -> BytesMut {
let mut buffers = self.buffers.lock().await;
if let Some(mut buffer) = buffers.pop() {
// Reuse existing buffer
buffer.clear(); // Keep capacity, clear content
@@ -53,14 +53,14 @@ impl BytesBufferPool {
BytesMut::with_capacity(self.buffer_size)
}
}
/// Return a buffer to the pool for reuse
pub async fn return_buffer(&self, mut buffer: BytesMut) {
// Reset the buffer for reuse
buffer.clear();
let mut buffers = self.buffers.lock().await;
// Only keep up to max_buffers
if buffers.len() < self.max_buffers {
buffers.push(buffer);
@@ -85,12 +85,12 @@ impl ParallelFileProcessor {
/// Creates a new processor instance
pub fn new(config: AppConfig) -> Self {
let concurrency_limiter = Arc::new(Semaphore::new(config.concurrency.max_concurrent_io));
// Create BytesMut pool for efficient operations
let chunk_size = config.resources.chunk_size_bytes;
let max_chunks = config.concurrency.max_parallel_chunks;
let bytes_pool = Arc::new(BytesBufferPool::new(chunk_size, max_chunks * 2));
Self {
config,
concurrency_limiter,
@@ -98,16 +98,16 @@ impl ParallelFileProcessor {
bytes_pool,
}
}
/// Creates a new processor instance with a buffer pool
pub fn new_with_buffer_pool(config: AppConfig, buffer_pool: Arc<BufferPool>) -> Self {
let concurrency_limiter = Arc::new(Semaphore::new(config.concurrency.max_concurrent_io));
// Create BytesMut pool for efficient operations
let chunk_size = config.resources.chunk_size_bytes;
let max_chunks = config.concurrency.max_parallel_chunks;
let bytes_pool = Arc::new(BytesBufferPool::new(chunk_size, max_chunks * 2));
Self {
config,
concurrency_limiter,
@@ -115,34 +115,39 @@ impl ParallelFileProcessor {
bytes_pool,
}
}
/// Divides a file into chunks for parallel processing
pub fn calculate_chunks(&self, file_size: u64) -> Vec<ChunkRange> {
// Determine if the file needs parallel processing
let needs_parallel = self.config.resources.needs_parallel_processing(
file_size, &self.config.concurrency
);
let needs_parallel = self
.config
.resources
.needs_parallel_processing(file_size, &self.config.concurrency);
if !needs_parallel {
// For small files, use a single chunk
return vec![ChunkRange {
return vec![ChunkRange {
index: 0,
start: 0,
size: file_size as usize
size: file_size as usize,
}];
}
// Calculate optimal number of chunks
let chunk_count = self.config.resources.calculate_optimal_chunks(
file_size, &self.config.concurrency
);
let chunk_count = self
.config
.resources
.calculate_optimal_chunks(file_size, &self.config.concurrency);
// Calculate size of each chunk
let chunk_size = self.config.resources.calculate_chunk_size(file_size, chunk_count);
let chunk_size = self
.config
.resources
.calculate_chunk_size(file_size, chunk_count);
// Create chunk ranges
let mut chunks = Vec::with_capacity(chunk_count);
let mut start = 0;
for i in 0..chunk_count {
let current_chunk_size = if i == chunk_count - 1 {
@@ -151,275 +156,328 @@ impl ParallelFileProcessor {
} else {
chunk_size
};
chunks.push(ChunkRange {
index: i,
start,
size: current_chunk_size,
});
start += current_chunk_size as u64;
}
debug!("File size: {} bytes, divided into {} chunks of ~{} bytes each",
file_size, chunks.len(), chunk_size);
debug!(
"File size: {} bytes, divided into {} chunks of ~{} bytes each",
file_size,
chunks.len(),
chunk_size
);
chunks
}
/// Reads a file in parallel and returns the complete content
/// Optimized implementation using BytesMut to reduce memory copies
pub async fn read_file_parallel(&self, file_path: &PathBuf) -> Result<Vec<u8>, FileRepositoryError> {
pub async fn read_file_parallel(
&self,
file_path: &PathBuf,
) -> Result<Vec<u8>, FileRepositoryError> {
// Get file size
let metadata = tokio::fs::metadata(file_path).await
let metadata = tokio::fs::metadata(file_path)
.await
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
let file_size = metadata.len();
// Check if the file is too large for memory
if !self.config.resources.can_load_in_memory(file_size) {
return Err(FileRepositoryError::Other(
format!("File too large to load in memory: {} MB (max: {} MB)",
file_size / (1024 * 1024),
self.config.resources.max_in_memory_file_size_mb)
));
return Err(FileRepositoryError::Other(format!(
"File too large to load in memory: {} MB (max: {} MB)",
file_size / (1024 * 1024),
self.config.resources.max_in_memory_file_size_mb
)));
}
// Calculate chunks
let chunks = self.calculate_chunks(file_size);
if chunks.len() == 1 {
// For a single chunk, use simple reading with buffer pool if available
info!("Reading file with size {}MB as a single chunk", file_size / (1024 * 1024));
info!(
"Reading file with size {}MB as a single chunk",
file_size / (1024 * 1024)
);
if let Some(pool) = &self.buffer_pool {
// Use buffer from the pool for efficient reading
debug!("Using buffer pool for single chunk read");
let mut buffer = pool.get_buffer().await;
// If the buffer is too small, revert to standard implementation
if buffer.capacity() < file_size as usize {
debug!("Buffer from pool too small ({}), using standard read", buffer.capacity());
let content = tokio::fs::read(file_path).await
debug!(
"Buffer from pool too small ({}), using standard read",
buffer.capacity()
);
let content = tokio::fs::read(file_path)
.await
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
return Ok(content);
}
// Use memory buffer from the pool
let mut file = File::open(file_path).await
let mut file = File::open(file_path)
.await
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
let read_size = file.read(buffer.as_mut_slice()).await
let read_size = file
.read(buffer.as_mut_slice())
.await
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
buffer.set_used(read_size);
// Convert to Vec<u8>
let content = buffer.into_vec();
return Ok(content);
} else {
// Standard implementation without pool
let content = tokio::fs::read(file_path).await
let content = tokio::fs::read(file_path)
.await
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
return Ok(content);
}
}
// For multiple chunks, use parallel reading
info!("Reading file with size {}MB in {} parallel chunks using BytesMut",
file_size / (1024 * 1024), chunks.len());
info!(
"Reading file with size {}MB in {} parallel chunks using BytesMut",
file_size / (1024 * 1024),
chunks.len()
);
// Create final result buffer (pre-allocated)
let mut result = BytesMut::with_capacity(file_size as usize);
result.resize(file_size as usize, 0);
let result_mutex = Arc::new(Mutex::new(result));
// Create tasks for each chunk
let mut tasks = Vec::with_capacity(chunks.len());
// Open file once and share it
let file = Arc::new(File::open(file_path).await
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?);
let file = Arc::new(
File::open(file_path)
.await
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?,
);
// Reference to BytesMut pool
let bytes_pool = self.bytes_pool.clone();
// Process chunks in parallel
for chunk in chunks {
let file_clone = file.clone();
let result_clone = result_mutex.clone();
let semaphore_clone = self.concurrency_limiter.clone();
let bytes_pool_clone = bytes_pool.clone();
// Spawn task for this chunk - no need to copy the original data
let task = task::spawn(async move {
// Acquire semaphore permit
let _permit = semaphore_clone.acquire().await.unwrap();
// Get a reusable buffer from the BytesMut pool
let mut chunk_buffer = bytes_pool_clone.get_buffer().await;
// Ensure it has sufficient capacity
if chunk_buffer.capacity() < chunk.size {
chunk_buffer = BytesMut::with_capacity(chunk.size);
}
// Resize to the exact size needed
chunk_buffer.resize(chunk.size, 0);
// Create a duplicate file descriptor for independent use
let mut file_handle = file_clone.try_clone().await?;
// Position and read directly into the BytesMut
file_handle.seek(SeekFrom::Start(chunk.start)).await?;
let bytes_read = file_handle.read_exact(&mut chunk_buffer[..chunk.size]).await?;
let bytes_read = file_handle
.read_exact(&mut chunk_buffer[..chunk.size])
.await?;
if bytes_read != chunk.size {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("Expected to read {} bytes but got {}", chunk.size, bytes_read)
format!(
"Expected to read {} bytes but got {}",
chunk.size, bytes_read
),
));
}
// Write to final result
let mut result_lock = result_clone.lock().await;
let start_pos = chunk.start as usize;
let end_pos = start_pos + chunk.size;
// Use copy_from_slice to copy from BytesMut to result buffer
result_lock[start_pos..end_pos].copy_from_slice(&chunk_buffer[..chunk.size]);
// Return the buffer to the pool for reuse
bytes_pool_clone.return_buffer(chunk_buffer).await;
// Log progress
debug!("Chunk {} processed: {} bytes from offset {}",
chunk.index, chunk.size, chunk.start);
debug!(
"Chunk {} processed: {} bytes from offset {}",
chunk.index, chunk.size, chunk.start
);
Ok::<_, io::Error>(())
});
tasks.push(task);
}
// Wait for all tasks to complete
let results = join_all(tasks).await;
// Check for errors
for (i, task_result) in results.into_iter().enumerate() {
match task_result {
Ok(Ok(())) => {},
Ok(Ok(())) => {}
Ok(Err(e)) => {
error!("Error in chunk {}: {}", i, e);
return Err(FileRepositoryError::StorageError(e.to_string()));
},
}
Err(e) => {
error!("Task error in chunk {}: {}", i, e);
return Err(FileRepositoryError::Other(format!("Task error: {}", e)));
}
}
}
// Get the final result and convert to Vec<u8>
let result_buffer = result_mutex.lock().await;
let result_vec = result_buffer.to_vec();
info!("Successfully read file of {}MB in parallel with optimized BytesMut", file_size / (1024 * 1024));
info!(
"Successfully read file of {}MB in parallel with optimized BytesMut",
file_size / (1024 * 1024)
);
Ok(result_vec)
}
/// Writes a file in parallel from a buffer
/// Optimized implementation using BytesMut/Bytes to reduce memory copies
pub async fn write_file_parallel(
&self,
file_path: &PathBuf,
content: &[u8]
&self,
file_path: &PathBuf,
content: &[u8],
) -> Result<(), FileRepositoryError> {
let file_size = content.len() as u64;
// Calculate chunks
let chunks = self.calculate_chunks(file_size);
if chunks.len() == 1 {
// For a single chunk, use simple writing
info!("Writing file with size {}MB as a single chunk", file_size / (1024 * 1024));
info!(
"Writing file with size {}MB as a single chunk",
file_size / (1024 * 1024)
);
// Standard implementation (buffer pooling offers no advantages for simple writing)
tokio::fs::write(file_path, content).await
tokio::fs::write(file_path, content)
.await
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
return Ok(());
}
// For multiple chunks, use parallel writing
info!("Writing file with size {}MB in {} parallel chunks using Bytes",
file_size / (1024 * 1024), chunks.len());
info!(
"Writing file with size {}MB in {} parallel chunks using Bytes",
file_size / (1024 * 1024),
chunks.len()
);
// Create file (we don't use Mutex to reduce contention)
let file = File::create(file_path).await
let file = File::create(file_path)
.await
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
// Convert content to Bytes (single copy step)
let content_bytes = Bytes::copy_from_slice(content);
// Create tasks for each chunk
let mut tasks = Vec::with_capacity(chunks.len());
// Process chunks in parallel
for chunk in chunks {
let file_clone = file.try_clone().await
let file_clone = file
.try_clone()
.await
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
let semaphore_clone = self.concurrency_limiter.clone();
// Create Bytes slice (doesn't copy data, only references)
let start_idx = chunk.start as usize;
let end_idx = start_idx + chunk.size;
let chunk_data = content_bytes.slice(start_idx..end_idx);
// Create and launch task
let task = task::spawn(async move {
// Acquire semaphore permit
let _permit = semaphore_clone.acquire().await.unwrap();
// Position and write
let mut file_handle = file_clone;
file_handle.seek(SeekFrom::Start(chunk.start)).await?;
file_handle.write_all(&chunk_data).await?;
// Log progress
debug!("Chunk {} written: {} bytes at offset {}",
chunk.index, chunk.size, chunk.start);
debug!(
"Chunk {} written: {} bytes at offset {}",
chunk.index, chunk.size, chunk.start
);
Ok::<_, io::Error>(())
});
tasks.push(task);
}
// Wait for all tasks to complete
let results = join_all(tasks).await;
// Check for errors
for (i, task_result) in results.into_iter().enumerate() {
match task_result {
Ok(Ok(())) => {},
Ok(Ok(())) => {}
Ok(Err(e)) => {
error!("Error in chunk {}: {}", i, e);
return Err(FileRepositoryError::StorageError(e.to_string()));
},
}
Err(e) => {
error!("Task error in chunk {}: {}", i, e);
return Err(FileRepositoryError::Other(format!("Task error: {}", e)));
}
}
}
// Ensure everything has been written correctly
let mut file_handle = file;
file_handle.flush().await.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
info!("Successfully wrote file of {}MB in parallel with optimized Bytes", file_size / (1024 * 1024));
file_handle
.flush()
.await
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
info!(
"Successfully wrote file of {}MB in parallel with optimized Bytes",
file_size / (1024 * 1024)
);
Ok(())
}
}
@@ -429,59 +487,62 @@ mod tests {
use super::*;
use bytes::BufMut;
use tempfile::tempdir;
#[tokio::test]
async fn test_parallel_read_write() {
// Create configuration with low threshold for testing
let mut config = AppConfig::default();
config.concurrency.min_size_for_parallel_chunks_mb = 1; // 1MB for testing
config.concurrency.max_parallel_chunks = 4;
let processor = ParallelFileProcessor::new(config);
// Create temporary directory
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.bin");
// Create test data (2MB)
let size = 2 * 1024 * 1024;
let mut test_data = Vec::with_capacity(size);
for i in 0..size {
test_data.push((i % 256) as u8);
}
// Write file in parallel
processor.write_file_parallel(&file_path, &test_data).await.unwrap();
processor
.write_file_parallel(&file_path, &test_data)
.await
.unwrap();
// Read file in parallel
let read_data = processor.read_file_parallel(&file_path).await.unwrap();
// Verify that the data is identical
assert_eq!(test_data.len(), read_data.len());
assert_eq!(test_data, read_data);
}
#[tokio::test]
async fn test_bytesmut_pool() {
// Create pool
let pool = BytesBufferPool::new(1024, 5);
// Get buffer
let mut buffer1 = pool.get_buffer().await;
buffer1.put_slice(b"test data");
assert_eq!(&buffer1[..9], b"test data");
// Return buffer to the pool
pool.return_buffer(buffer1).await;
// Get another buffer (should be the same one)
let buffer2 = pool.get_buffer().await;
assert_eq!(buffer2.capacity(), 1024);
// The buffer should be empty (cleared)
assert_eq!(buffer2.len(), 0);
}
#[test]
fn test_chunk_calculation() {
// Create test configuration
@@ -489,22 +550,22 @@ mod tests {
config.concurrency.min_size_for_parallel_chunks_mb = 100; // 100MB
config.concurrency.max_parallel_chunks = 4;
config.concurrency.parallel_chunk_size_bytes = 50 * 1024 * 1024; // 50MB
let processor = ParallelFileProcessor::new(config);
// Small file (10MB)
let small_file_size = 10 * 1024 * 1024;
let chunks = processor.calculate_chunks(small_file_size);
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].size as u64, small_file_size);
// Large file (300MB)
let large_file_size = 300 * 1024 * 1024;
let chunks = processor.calculate_chunks(large_file_size);
assert_eq!(chunks.len(), 4); // Limited to max_parallel_chunks
// Verify that all chunks add up to the total size
let total_size: u64 = chunks.iter().map(|c| c.size as u64).sum();
assert_eq!(total_size, large_file_size);
}
}
}
@@ -3,9 +3,11 @@ use chrono::Utc;
use sqlx::{PgPool, Row, types::Uuid};
use std::sync::Arc;
use crate::domain::entities::contact::AddressBook;
use crate::domain::repositories::address_book_repository::{AddressBookRepository, AddressBookRepositoryResult};
use crate::common::errors::DomainError;
use crate::domain::entities::contact::AddressBook;
use crate::domain::repositories::address_book_repository::{
AddressBookRepository, AddressBookRepositoryResult,
};
pub struct AddressBookPgRepository {
pool: Arc<PgPool>,
@@ -19,7 +21,10 @@ impl AddressBookPgRepository {
#[async_trait]
impl AddressBookRepository for AddressBookPgRepository {
async fn create_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook> {
async fn create_address_book(
&self,
address_book: AddressBook,
) -> AddressBookRepositoryResult<AddressBook> {
let row = sqlx::query(
r#"
INSERT INTO carddav.address_books (id, name, owner_id, description, color, is_public, created_at, updated_at)
@@ -51,7 +56,10 @@ impl AddressBookRepository for AddressBookPgRepository {
))
}
async fn update_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook> {
async fn update_address_book(
&self,
address_book: AddressBook,
) -> AddressBookRepositoryResult<AddressBook> {
let now = Utc::now();
let row = sqlx::query(
r#"
@@ -59,7 +67,7 @@ impl AddressBookRepository for AddressBookPgRepository {
SET name = $1, description = $2, color = $3, is_public = $4, updated_at = $5
WHERE id = $6
RETURNING id, name, owner_id, description, color, is_public, created_at, updated_at
"#
"#,
)
.bind(address_book.name())
.bind(address_book.description())
@@ -69,7 +77,9 @@ impl AddressBookRepository for AddressBookPgRepository {
.bind(address_book.id())
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to update address book: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to update address book: {}", e))
})?;
Ok(AddressBook::from_raw(
row.get("id"),
@@ -88,59 +98,38 @@ impl AddressBookRepository for AddressBookPgRepository {
r#"
DELETE FROM carddav.address_books
WHERE id = $1
"#
"#,
)
.bind(id)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to delete address book: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to delete address book: {}", e))
})?;
Ok(())
}
async fn get_address_book_by_id(&self, id: &Uuid) -> AddressBookRepositoryResult<Option<AddressBook>> {
async fn get_address_book_by_id(
&self,
id: &Uuid,
) -> AddressBookRepositoryResult<Option<AddressBook>> {
let maybe_row = sqlx::query(
r#"
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
FROM carddav.address_books
WHERE id = $1
"#
"#,
)
.bind(id)
.fetch_optional(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get address book by id: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get address book by id: {}", e))
})?;
let result = maybe_row.map(|row| AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
));
Ok(result)
}
async fn get_address_books_by_owner(&self, owner_id: &str) -> AddressBookRepositoryResult<Vec<AddressBook>> {
let rows = sqlx::query(
r#"
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
FROM carddav.address_books
WHERE owner_id = $1
ORDER BY name
"#
)
.bind(owner_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get address books by owner: {}", e)))?;
let result = rows.into_iter()
.map(|row| AddressBook::from_raw(
let result = maybe_row.map(|row| {
AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
@@ -149,13 +138,54 @@ impl AddressBookRepository for AddressBookPgRepository {
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
))
)
});
Ok(result)
}
async fn get_address_books_by_owner(
&self,
owner_id: &str,
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
let rows = sqlx::query(
r#"
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
FROM carddav.address_books
WHERE owner_id = $1
ORDER BY name
"#,
)
.bind(owner_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| {
DomainError::database_error(format!("Failed to get address books by owner: {}", e))
})?;
let result = rows
.into_iter()
.map(|row| {
AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
)
})
.collect();
Ok(result)
}
async fn get_shared_address_books(&self, user_id: &str) -> AddressBookRepositoryResult<Vec<AddressBook>> {
async fn get_shared_address_books(
&self,
user_id: &str,
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
let rows = sqlx::query(
r#"
SELECT a.id, a.name, a.owner_id, a.description, a.color, a.is_public, a.created_at, a.updated_at
@@ -170,17 +200,20 @@ impl AddressBookRepository for AddressBookPgRepository {
.await
.map_err(|e| DomainError::database_error(format!("Failed to get shared address books: {}", e)))?;
let result = rows.into_iter()
.map(|row| AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
))
let result = rows
.into_iter()
.map(|row| {
AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
)
})
.collect();
Ok(result)
@@ -193,35 +226,45 @@ impl AddressBookRepository for AddressBookPgRepository {
FROM carddav.address_books
WHERE is_public = true
ORDER BY name
"#
"#,
)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get public address books: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get public address books: {}", e))
})?;
let result = rows.into_iter()
.map(|row| AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
))
let result = rows
.into_iter()
.map(|row| {
AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
)
})
.collect();
Ok(result)
}
async fn share_address_book(&self, address_book_id: &Uuid, user_id: &str, can_write: bool) -> AddressBookRepositoryResult<()> {
async fn share_address_book(
&self,
address_book_id: &Uuid,
user_id: &str,
can_write: bool,
) -> AddressBookRepositoryResult<()> {
sqlx::query(
r#"
INSERT INTO carddav.address_book_shares (address_book_id, user_id, can_write)
VALUES ($1, $2, $3)
ON CONFLICT (address_book_id, user_id) DO UPDATE SET can_write = $3
"#
"#,
)
.bind(address_book_id)
.bind(user_id)
@@ -233,40 +276,52 @@ impl AddressBookRepository for AddressBookPgRepository {
Ok(())
}
async fn unshare_address_book(&self, address_book_id: &Uuid, user_id: &str) -> AddressBookRepositoryResult<()> {
async fn unshare_address_book(
&self,
address_book_id: &Uuid,
user_id: &str,
) -> AddressBookRepositoryResult<()> {
sqlx::query(
r#"
DELETE FROM carddav.address_book_shares
WHERE address_book_id = $1 AND user_id = $2
"#
"#,
)
.bind(address_book_id)
.bind(user_id)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to unshare address book: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to unshare address book: {}", e))
})?;
Ok(())
}
async fn get_address_book_shares(&self, address_book_id: &Uuid) -> AddressBookRepositoryResult<Vec<(String, bool)>> {
async fn get_address_book_shares(
&self,
address_book_id: &Uuid,
) -> AddressBookRepositoryResult<Vec<(String, bool)>> {
let rows = sqlx::query(
r#"
SELECT user_id, can_write
FROM carddav.address_book_shares
WHERE address_book_id = $1
ORDER BY user_id
"#
"#,
)
.bind(address_book_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get address book shares: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get address book shares: {}", e))
})?;
let result = rows.into_iter()
let result = rows
.into_iter()
.map(|row| (row.get("user_id"), row.get("can_write")))
.collect();
Ok(result)
}
}
}
@@ -3,9 +3,11 @@ use chrono::{DateTime, Utc};
use sqlx::{PgPool, Row, types::Uuid};
use std::sync::Arc;
use crate::domain::entities::calendar_event::CalendarEvent;
use crate::domain::repositories::calendar_event_repository::{CalendarEventRepository, CalendarEventRepositoryResult};
use crate::common::errors::DomainError;
use crate::domain::entities::calendar_event::CalendarEvent;
use crate::domain::repositories::calendar_event_repository::{
CalendarEventRepository, CalendarEventRepositoryResult,
};
pub struct CalendarEventPgRepository {
pool: Arc<PgPool>,
@@ -19,11 +21,14 @@ impl CalendarEventPgRepository {
#[async_trait]
impl CalendarEventRepository for CalendarEventPgRepository {
async fn create_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent> {
async fn create_event(
&self,
event: CalendarEvent,
) -> CalendarEventRepositoryResult<CalendarEvent> {
// This method would need a full implementation that builds the CalendarEvent
// from the query result, using constructor methods
// For this demonstration, we return the same event
sqlx::query(
r#"
INSERT INTO caldav.calendar_events (
@@ -31,7 +36,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
all_day, rrule, created_at, updated_at, ical_uid, ical_data
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
"#
"#,
)
.bind(event.id())
.bind(event.calendar_id())
@@ -48,15 +53,20 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.bind(event.ical_data())
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to create calendar event: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to create calendar event: {}", e))
})?;
// We return the same event instead of a result
Ok(event)
}
async fn update_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent> {
async fn update_event(
&self,
event: CalendarEvent,
) -> CalendarEventRepositoryResult<CalendarEvent> {
let now = Utc::now();
sqlx::query(
r#"
UPDATE caldav.calendar_events
@@ -70,7 +80,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
ical_data = $8,
updated_at = $9
WHERE id = $10
"#
"#,
)
.bind(event.summary())
.bind(event.description())
@@ -84,7 +94,9 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.bind(event.id())
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to update calendar event: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to update calendar event: {}", e))
})?;
// In a full implementation, we would retrieve the updated event
// For simplicity, we return the same event we received
@@ -96,21 +108,23 @@ impl CalendarEventRepository for CalendarEventPgRepository {
r#"
DELETE FROM caldav.calendar_events
WHERE id = $1
"#
"#,
)
.bind(id)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to delete calendar event: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to delete calendar event: {}", e))
})?;
Ok(())
}
async fn get_events_in_time_range(
&self,
calendar_id: &Uuid,
start: &DateTime<Utc>,
end: &DateTime<Utc>
&self,
calendar_id: &Uuid,
start: &DateTime<Utc>,
end: &DateTime<Utc>,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
let rows = sqlx::query(
r#"
@@ -127,14 +141,16 @@ impl CalendarEventRepository for CalendarEventPgRepository {
(rrule IS NOT NULL AND end_time >= $2)
)
ORDER BY start_time
"#
"#,
)
.bind(calendar_id)
.bind(start)
.bind(end)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get events in time range: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get events in time range: {}", e))
})?;
let mut events = Vec::new();
for row in rows {
@@ -152,10 +168,13 @@ impl CalendarEventRepository for CalendarEventPgRepository {
row.get("ical_data"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
events.push(event);
}
Ok(events)
}
@@ -168,18 +187,20 @@ impl CalendarEventRepository for CalendarEventPgRepository {
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)))?
.map_err(|e| {
DomainError::database_error(format!("Failed to get calendar event by id: {}", e))
})?
.ok_or_else(|| DomainError::not_found("Calendar Event", id.to_string()))?;
// In a real implementation, we would build a complete CalendarEvent object
// For simplicity, we create an object with default values to
// demonstrate the approach without macros
let event = CalendarEvent::with_id(
row.get("id"),
row.get("calendar_id"),
@@ -193,13 +214,19 @@ impl CalendarEventRepository for CalendarEventPgRepository {
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)))?;
row.get("updated_at"),
)
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
Ok(event)
}
async fn list_events_by_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
async fn list_events_by_calendar(
&self,
calendar_id: &Uuid,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
let rows = sqlx::query(
r#"
SELECT
@@ -209,12 +236,14 @@ impl CalendarEventRepository for CalendarEventPgRepository {
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)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get events by calendar: {}", e))
})?;
let mut events = Vec::new();
for row in rows {
@@ -232,16 +261,23 @@ impl CalendarEventRepository for CalendarEventPgRepository {
row.get("ical_data"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
events.push(event);
}
Ok(events)
}
async fn find_events_by_summary(&self, calendar_id: &Uuid, summary: &str) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
async fn find_events_by_summary(
&self,
calendar_id: &Uuid,
summary: &str,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
let search_pattern = format!("%{}%", summary);
let rows = sqlx::query(
r#"
SELECT
@@ -251,13 +287,15 @@ impl CalendarEventRepository for CalendarEventPgRepository {
FROM caldav.calendar_events
WHERE calendar_id = $1 AND summary ILIKE $2
ORDER BY start_time
"#
"#,
)
.bind(calendar_id)
.bind(&search_pattern)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to find events by summary: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to find events by summary: {}", e))
})?;
let mut events = Vec::new();
for row in rows {
@@ -275,14 +313,21 @@ impl CalendarEventRepository for CalendarEventPgRepository {
row.get("ical_data"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
events.push(event);
}
Ok(events)
}
async fn find_event_by_ical_uid(&self, calendar_id: &Uuid, ical_uid: &str) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
async fn find_event_by_ical_uid(
&self,
calendar_id: &Uuid,
ical_uid: &str,
) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
let row_opt = sqlx::query(
r#"
SELECT
@@ -291,13 +336,15 @@ impl CalendarEventRepository for CalendarEventPgRepository {
created_at, updated_at, ical_uid, ical_data
FROM caldav.calendar_events
WHERE calendar_id = $1 AND ical_uid = $2
"#
"#,
)
.bind(calendar_id)
.bind(ical_uid)
.fetch_optional(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by UID: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get calendar event by UID: {}", e))
})?;
match row_opt {
Some(row) => {
@@ -315,49 +362,62 @@ impl CalendarEventRepository for CalendarEventPgRepository {
row.get("ical_data"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
Ok(Some(event))
}
None => Ok(None),
}
}
async fn count_events_in_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<i64> {
async fn count_events_in_calendar(
&self,
calendar_id: &Uuid,
) -> CalendarEventRepositoryResult<i64> {
let row = sqlx::query(
r#"
SELECT COUNT(*) as count
FROM caldav.calendar_events
WHERE calendar_id = $1
"#
"#,
)
.bind(calendar_id)
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to count events in calendar: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to count events in calendar: {}", e))
})?;
Ok(row.get::<i64, _>("count"))
}
async fn delete_all_events_in_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<i64> {
async fn delete_all_events_in_calendar(
&self,
calendar_id: &Uuid,
) -> CalendarEventRepositoryResult<i64> {
let result = sqlx::query(
r#"
DELETE FROM caldav.calendar_events
WHERE calendar_id = $1
"#
"#,
)
.bind(calendar_id)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to delete all events in calendar: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to delete all events in calendar: {}", e))
})?;
Ok(result.rows_affected() as i64)
}
async fn list_events_by_calendar_paginated(
&self,
&self,
calendar_id: &Uuid,
limit: i64,
offset: i64
offset: i64,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
let rows = sqlx::query(
r#"
@@ -369,14 +429,19 @@ impl CalendarEventRepository for CalendarEventPgRepository {
WHERE calendar_id = $1
ORDER BY start_time
LIMIT $2 OFFSET $3
"#
"#,
)
.bind(calendar_id)
.bind(limit)
.bind(offset)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get paginated events by calendar: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!(
"Failed to get paginated events by calendar: {}",
e
))
})?;
let mut events = Vec::new();
for row in rows {
@@ -394,18 +459,21 @@ impl CalendarEventRepository for CalendarEventPgRepository {
row.get("ical_data"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
events.push(event);
}
Ok(events)
}
async fn find_recurring_events_in_range(
&self,
calendar_id: &Uuid,
start: &DateTime<Utc>,
end: &DateTime<Utc>
end: &DateTime<Utc>,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
let rows = sqlx::query(
r#"
@@ -419,14 +487,16 @@ impl CalendarEventRepository for CalendarEventPgRepository {
AND end_time >= $2
AND start_time <= $3
ORDER BY start_time
"#
"#,
)
.bind(calendar_id)
.bind(start)
.bind(end)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to find recurring events in range: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to find recurring events in range: {}", e))
})?;
let mut events = Vec::new();
for row in rows {
@@ -444,10 +514,13 @@ impl CalendarEventRepository for CalendarEventPgRepository {
row.get("ical_data"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
events.push(event);
}
Ok(events)
}
}
}
@@ -3,9 +3,11 @@ use chrono::Utc;
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;
use crate::domain::entities::calendar::Calendar;
use crate::domain::repositories::calendar_repository::{
CalendarRepository, CalendarRepositoryResult,
};
pub struct CalendarPgRepository {
pool: Arc<PgPool>,
@@ -38,7 +40,7 @@ impl CalendarRepository for CalendarPgRepository {
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to create calendar: {}", e)))?;
// Build the Calendar object using its with_id constructor
let result = Calendar::with_id(
row.get("id"),
@@ -48,7 +50,10 @@ impl CalendarRepository for CalendarPgRepository {
row.get("color"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Failed to create calendar object: {}", e))
})?;
Ok(result)
}
@@ -61,7 +66,7 @@ impl CalendarRepository for CalendarPgRepository {
SET name = $1, description = $2, color = $3, is_public = $4, updated_at = $5
WHERE id = $6
RETURNING id, name, owner_id, description, color, is_public, created_at, updated_at
"#
"#,
)
.bind(calendar.name())
.bind(calendar.description())
@@ -72,7 +77,7 @@ impl CalendarRepository for CalendarPgRepository {
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to update calendar: {}", e)))?;
// Build the Calendar object using its with_id constructor
let result = Calendar::with_id(
row.get("id"),
@@ -82,7 +87,10 @@ impl CalendarRepository for CalendarPgRepository {
row.get("color"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Failed to create calendar object: {}", e))
})?;
Ok(result)
}
@@ -92,7 +100,7 @@ impl CalendarRepository for CalendarPgRepository {
r#"
DELETE FROM caldav.calendars
WHERE id = $1
"#
"#,
)
.bind(id)
.execute(&*self.pool)
@@ -108,7 +116,7 @@ impl CalendarRepository for CalendarPgRepository {
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
FROM caldav.calendars
WHERE id = $1
"#
"#,
)
.bind(id)
.fetch_optional(&*self.pool)
@@ -124,24 +132,32 @@ impl CalendarRepository for CalendarPgRepository {
row.get("color"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Failed to create calendar object: {}", e))
})?;
Ok(calendar)
}
async fn list_calendars_by_owner(&self, owner_id: &str) -> CalendarRepositoryResult<Vec<Calendar>> {
async fn list_calendars_by_owner(
&self,
owner_id: &str,
) -> CalendarRepositoryResult<Vec<Calendar>> {
let rows = sqlx::query(
r#"
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
FROM caldav.calendars
WHERE owner_id = $1
ORDER BY name
"#
"#,
)
.bind(owner_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get calendars by owner: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get calendars by owner: {}", e))
})?;
let mut calendars = Vec::new();
for row in rows {
@@ -153,27 +169,38 @@ impl CalendarRepository for CalendarPgRepository {
row.get("color"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Failed to create calendar object: {}", e))
})?;
calendars.push(calendar);
}
Ok(calendars)
}
async fn find_calendar_by_name_and_owner(&self, name: &str, owner_id: &str) -> CalendarRepositoryResult<Calendar> {
async fn find_calendar_by_name_and_owner(
&self,
name: &str,
owner_id: &str,
) -> CalendarRepositoryResult<Calendar> {
let row = sqlx::query(
r#"
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
FROM caldav.calendars
WHERE name = $1 AND owner_id = $2
"#
"#,
)
.bind(name)
.bind(owner_id)
.fetch_optional(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to find calendar by name and owner: {}", e)))?
.ok_or_else(|| DomainError::not_found("Calendar", format!("{} (owned by {})", name, owner_id)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to find calendar by name and owner: {}", e))
})?
.ok_or_else(|| {
DomainError::not_found("Calendar", format!("{} (owned by {})", name, owner_id))
})?;
let calendar = Calendar::with_id(
row.get("id"),
@@ -183,12 +210,18 @@ impl CalendarRepository for CalendarPgRepository {
row.get("color"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Failed to create calendar object: {}", e))
})?;
Ok(calendar)
}
async fn list_calendars_shared_with_user(&self, user_id: &str) -> CalendarRepositoryResult<Vec<Calendar>> {
async fn list_calendars_shared_with_user(
&self,
user_id: &str,
) -> CalendarRepositoryResult<Vec<Calendar>> {
let rows = sqlx::query(
r#"
SELECT c.id, c.name, c.owner_id, c.description, c.color, c.is_public, c.created_at, c.updated_at
@@ -213,14 +246,21 @@ impl CalendarRepository for CalendarPgRepository {
row.get("color"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Failed to create calendar object: {}", e))
})?;
calendars.push(calendar);
}
Ok(calendars)
}
async fn list_public_calendars(&self, limit: i64, offset: i64) -> CalendarRepositoryResult<Vec<Calendar>> {
async fn list_public_calendars(
&self,
limit: i64,
offset: i64,
) -> CalendarRepositoryResult<Vec<Calendar>> {
let rows = sqlx::query(
r#"
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
@@ -228,13 +268,15 @@ impl CalendarRepository for CalendarPgRepository {
WHERE is_public = true
ORDER BY name
LIMIT $1 OFFSET $2
"#
"#,
)
.bind(limit)
.bind(offset)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get public calendars: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get public calendars: {}", e))
})?;
let mut calendars = Vec::new();
for row in rows {
@@ -243,17 +285,24 @@ impl CalendarRepository for CalendarPgRepository {
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("color"),
row.get("created_at"),
row.get("updated_at"),
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
)
.map_err(|e| {
DomainError::database_error(format!("Failed to create calendar object: {}", e))
})?;
calendars.push(calendar);
}
Ok(calendars)
}
async fn user_has_calendar_access(&self, calendar_id: &Uuid, user_id: &str) -> CalendarRepositoryResult<bool> {
async fn user_has_calendar_access(
&self,
calendar_id: &Uuid,
user_id: &str,
) -> CalendarRepositoryResult<bool> {
// Check if the user is the owner of the calendar or has a share
let row = sqlx::query(
r#"
@@ -264,31 +313,39 @@ impl CalendarRepository for CalendarPgRepository {
SELECT 1 FROM caldav.calendar_shares s
WHERE s.calendar_id = $1 AND s.user_id = $2
) as has_access
"#
"#,
)
.bind(calendar_id)
.bind(user_id)
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to check calendar access: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to check calendar access: {}", e))
})?;
Ok(row.get::<bool, _>("has_access"))
}
async fn share_calendar(&self, calendar_id: &Uuid, user_id: &str, access_level: &str) -> CalendarRepositoryResult<()> {
async fn share_calendar(
&self,
calendar_id: &Uuid,
user_id: &str,
access_level: &str,
) -> CalendarRepositoryResult<()> {
// Validate access level
if !["read", "write", "owner"].contains(&access_level) {
return Err(DomainError::validation_error(
format!("Invalid access level: '{}'. Must be 'read', 'write', or 'owner'", access_level)
));
return Err(DomainError::validation_error(format!(
"Invalid access level: '{}'. Must be 'read', 'write', or 'owner'",
access_level
)));
}
sqlx::query(
r#"
INSERT INTO caldav.calendar_shares (calendar_id, user_id, access_level)
VALUES ($1, $2, $3)
ON CONFLICT (calendar_id, user_id) DO UPDATE SET access_level = $3
"#
"#,
)
.bind(calendar_id)
.bind(user_id)
@@ -300,12 +357,16 @@ impl CalendarRepository for CalendarPgRepository {
Ok(())
}
async fn remove_calendar_sharing(&self, calendar_id: &Uuid, user_id: &str) -> CalendarRepositoryResult<()> {
async fn remove_calendar_sharing(
&self,
calendar_id: &Uuid,
user_id: &str,
) -> CalendarRepositoryResult<()> {
sqlx::query(
r#"
DELETE FROM caldav.calendar_shares
WHERE calendar_id = $1 AND user_id = $2
"#
"#,
)
.bind(calendar_id)
.bind(user_id)
@@ -316,19 +377,24 @@ impl CalendarRepository for CalendarPgRepository {
Ok(())
}
async fn get_calendar_shares(&self, calendar_id: &Uuid) -> CalendarRepositoryResult<Vec<(String, String)>> {
async fn get_calendar_shares(
&self,
calendar_id: &Uuid,
) -> CalendarRepositoryResult<Vec<(String, String)>> {
let rows = sqlx::query(
r#"
SELECT user_id, access_level
FROM caldav.calendar_shares
WHERE calendar_id = $1
ORDER BY user_id
"#
"#,
)
.bind(calendar_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get calendar shares: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get calendar shares: {}", e))
})?;
let mut shares = Vec::new();
for row in rows {
@@ -337,76 +403,100 @@ impl CalendarRepository for CalendarPgRepository {
Ok(shares)
}
async fn get_calendar_property(&self, calendar_id: &Uuid, property_name: &str) -> CalendarRepositoryResult<Option<String>> {
async fn get_calendar_property(
&self,
calendar_id: &Uuid,
property_name: &str,
) -> CalendarRepositoryResult<Option<String>> {
let row = sqlx::query(
r#"
SELECT value
FROM caldav.calendar_properties
WHERE calendar_id = $1 AND name = $2
"#
"#,
)
.bind(calendar_id)
.bind(property_name)
.fetch_optional(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get calendar property: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get calendar property: {}", e))
})?;
Ok(row.map(|r| r.get("value")))
}
async fn set_calendar_property(&self, calendar_id: &Uuid, property_name: &str, property_value: &str) -> CalendarRepositoryResult<()> {
async fn set_calendar_property(
&self,
calendar_id: &Uuid,
property_name: &str,
property_value: &str,
) -> CalendarRepositoryResult<()> {
sqlx::query(
r#"
INSERT INTO caldav.calendar_properties (calendar_id, name, value)
VALUES ($1, $2, $3)
ON CONFLICT (calendar_id, name) DO UPDATE SET value = $3
"#
"#,
)
.bind(calendar_id)
.bind(property_name)
.bind(property_value)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to set calendar property: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to set calendar property: {}", e))
})?;
Ok(())
}
async fn remove_calendar_property(&self, calendar_id: &Uuid, property_name: &str) -> CalendarRepositoryResult<()> {
async fn remove_calendar_property(
&self,
calendar_id: &Uuid,
property_name: &str,
) -> CalendarRepositoryResult<()> {
sqlx::query(
r#"
DELETE FROM caldav.calendar_properties
WHERE calendar_id = $1 AND name = $2
"#
"#,
)
.bind(calendar_id)
.bind(property_name)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to remove calendar property: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to remove calendar property: {}", e))
})?;
Ok(())
}
async fn get_calendar_properties(&self, calendar_id: &Uuid) -> CalendarRepositoryResult<std::collections::HashMap<String, String>> {
async fn get_calendar_properties(
&self,
calendar_id: &Uuid,
) -> CalendarRepositoryResult<std::collections::HashMap<String, String>> {
let rows = sqlx::query(
r#"
SELECT name, value
FROM caldav.calendar_properties
WHERE calendar_id = $1
"#
"#,
)
.bind(calendar_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get calendar properties: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get calendar properties: {}", e))
})?;
let mut properties = std::collections::HashMap::new();
for row in rows {
properties.insert(row.get("name"), row.get("value"));
}
Ok(properties)
}
}
}
@@ -1,223 +1,276 @@
use async_trait::async_trait;
use sqlx::{PgPool, Row, types::Uuid};
use std::sync::Arc;
use chrono::Utc;
use serde_json::Value as JsonValue;
use crate::domain::entities::contact::{Contact, ContactGroup};
use crate::domain::repositories::contact_repository::{ContactGroupRepository, ContactRepositoryResult};
use crate::common::errors::{DomainError, ErrorKind};
use super::contact_persistence_dto::{
emails_from_persistence, phones_from_persistence, addresses_from_persistence,
EmailPersistenceDto, PhonePersistenceDto, AddressPersistenceDto,
};
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> {
sqlx::query(
"INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)"
)
.bind(group.id())
.bind(group.address_book_id())
.bind(group.name())
.bind(group.created_at())
.bind(group.updated_at())
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to create group: {}", e)))?;
Ok(group)
}
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
sqlx::query(
"UPDATE carddav.contact_groups SET name = $1, updated_at = $2 WHERE id = $3"
)
.bind(group.name())
.bind(Utc::now())
.bind(group.id())
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to update group: {}", e)))?;
Ok(group)
}
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> {
// Delete memberships first
sqlx::query("DELETE FROM carddav.group_memberships WHERE group_id = $1")
.bind(id)
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to delete group memberships: {}", e)))?;
sqlx::query("DELETE FROM carddav.contact_groups WHERE id = $1")
.bind(id)
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to delete group: {}", e)))?;
Ok(())
}
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>> {
let row = sqlx::query(
"SELECT id, address_book_id, name, created_at, updated_at FROM carddav.contact_groups WHERE id = $1"
)
.bind(id)
.fetch_optional(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to get group: {}", e)))?;
match row {
Some(row) => {
let group = ContactGroup::from_raw(
row.get::<Uuid, _>("id"),
row.get::<Uuid, _>("address_book_id"),
row.get::<String, _>("name"),
row.get("created_at"),
row.get("updated_at"),
);
Ok(Some(group))
},
None => Ok(None),
}
}
async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
let rows = sqlx::query(
"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.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to list groups: {}", e)))?;
Ok(rows.into_iter().map(|row| {
ContactGroup::from_raw(
row.get::<Uuid, _>("id"),
row.get::<Uuid, _>("address_book_id"),
row.get::<String, _>("name"),
row.get("created_at"),
row.get("updated_at"),
)
}).collect())
}
async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
sqlx::query(
"INSERT INTO carddav.group_memberships (group_id, contact_id) VALUES ($1, $2) ON CONFLICT DO NOTHING"
)
.bind(group_id)
.bind(contact_id)
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", 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(
"DELETE FROM carddav.group_memberships WHERE group_id = $1 AND contact_id = $2"
)
.bind(group_id)
.bind(contact_id)
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", 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 gm ON c.id = gm.contact_id
WHERE gm.group_id = $1
ORDER BY c.full_name, c.first_name, c.last_name
"#
)
.bind(group_id)
.fetch_all(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to get contacts in group: {}", e)))?;
let mut contacts = Vec::new();
for row in &rows {
let email_json: JsonValue = row.get("email");
let phone_json: JsonValue = row.get("phone");
let address_json: JsonValue = row.get("address");
let emails = serde_json::from_value::<Vec<EmailPersistenceDto>>(email_json)
.map(emails_from_persistence)
.unwrap_or_default();
let phones = serde_json::from_value::<Vec<PhonePersistenceDto>>(phone_json)
.map(phones_from_persistence)
.unwrap_or_default();
let addresses = serde_json::from_value::<Vec<AddressPersistenceDto>>(address_json)
.map(addresses_from_persistence)
.unwrap_or_default();
contacts.push(Contact::from_raw(
row.get("id"),
row.get("address_book_id"),
row.get("uid"),
row.get::<Option<String>, _>("full_name"),
row.get::<Option<String>, _>("first_name"),
row.get::<Option<String>, _>("last_name"),
row.get::<Option<String>, _>("nickname"),
emails,
phones,
addresses,
row.get::<Option<String>, _>("organization"),
row.get::<Option<String>, _>("title"),
row.get::<Option<String>, _>("notes"),
row.get::<Option<String>, _>("photo_url"),
row.get("birthday"),
row.get("anniversary"),
row.get("vcard"),
row.get("etag"),
row.get("created_at"),
row.get("updated_at"),
));
}
Ok(contacts)
}
async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
let rows = sqlx::query(
"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 gm ON g.id = gm.group_id WHERE gm.contact_id = $1 ORDER BY g.name"
)
.bind(contact_id)
.fetch_all(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to get groups for contact: {}", e)))?;
Ok(rows.into_iter().map(|row| {
ContactGroup::from_raw(
row.get::<Uuid, _>("id"),
row.get::<Uuid, _>("address_book_id"),
row.get::<String, _>("name"),
row.get("created_at"),
row.get("updated_at"),
)
}).collect())
}
}
use async_trait::async_trait;
use chrono::Utc;
use serde_json::Value as JsonValue;
use sqlx::{PgPool, Row, types::Uuid};
use std::sync::Arc;
use super::contact_persistence_dto::{
AddressPersistenceDto, EmailPersistenceDto, PhonePersistenceDto, addresses_from_persistence,
emails_from_persistence, phones_from_persistence,
};
use crate::common::errors::{DomainError, ErrorKind};
use crate::domain::entities::contact::{Contact, ContactGroup};
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> {
sqlx::query(
"INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)"
)
.bind(group.id())
.bind(group.address_book_id())
.bind(group.name())
.bind(group.created_at())
.bind(group.updated_at())
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to create group: {}", e)))?;
Ok(group)
}
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
sqlx::query("UPDATE carddav.contact_groups SET name = $1, updated_at = $2 WHERE id = $3")
.bind(group.name())
.bind(Utc::now())
.bind(group.id())
.execute(self.pool.as_ref())
.await
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"ContactGroup",
format!("Failed to update group: {}", e),
)
})?;
Ok(group)
}
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> {
// Delete memberships first
sqlx::query("DELETE FROM carddav.group_memberships WHERE group_id = $1")
.bind(id)
.execute(self.pool.as_ref())
.await
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"ContactGroup",
format!("Failed to delete group memberships: {}", e),
)
})?;
sqlx::query("DELETE FROM carddav.contact_groups WHERE id = $1")
.bind(id)
.execute(self.pool.as_ref())
.await
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"ContactGroup",
format!("Failed to delete group: {}", e),
)
})?;
Ok(())
}
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>> {
let row = sqlx::query(
"SELECT id, address_book_id, name, created_at, updated_at FROM carddav.contact_groups WHERE id = $1"
)
.bind(id)
.fetch_optional(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to get group: {}", e)))?;
match row {
Some(row) => {
let group = ContactGroup::from_raw(
row.get::<Uuid, _>("id"),
row.get::<Uuid, _>("address_book_id"),
row.get::<String, _>("name"),
row.get("created_at"),
row.get("updated_at"),
);
Ok(Some(group))
}
None => Ok(None),
}
}
async fn get_groups_by_address_book(
&self,
address_book_id: &Uuid,
) -> ContactRepositoryResult<Vec<ContactGroup>> {
let rows = sqlx::query(
"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.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to list groups: {}", e)))?;
Ok(rows
.into_iter()
.map(|row| {
ContactGroup::from_raw(
row.get::<Uuid, _>("id"),
row.get::<Uuid, _>("address_book_id"),
row.get::<String, _>("name"),
row.get("created_at"),
row.get("updated_at"),
)
})
.collect())
}
async fn add_contact_to_group(
&self,
group_id: &Uuid,
contact_id: &Uuid,
) -> ContactRepositoryResult<()> {
sqlx::query(
"INSERT INTO carddav.group_memberships (group_id, contact_id) VALUES ($1, $2) ON CONFLICT DO NOTHING"
)
.bind(group_id)
.bind(contact_id)
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", 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(
"DELETE FROM carddav.group_memberships WHERE group_id = $1 AND contact_id = $2",
)
.bind(group_id)
.bind(contact_id)
.execute(self.pool.as_ref())
.await
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"ContactGroup",
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 gm ON c.id = gm.contact_id
WHERE gm.group_id = $1
ORDER BY c.full_name, c.first_name, c.last_name
"#,
)
.bind(group_id)
.fetch_all(self.pool.as_ref())
.await
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"ContactGroup",
format!("Failed to get contacts in group: {}", e),
)
})?;
let mut contacts = Vec::new();
for row in &rows {
let email_json: JsonValue = row.get("email");
let phone_json: JsonValue = row.get("phone");
let address_json: JsonValue = row.get("address");
let emails = serde_json::from_value::<Vec<EmailPersistenceDto>>(email_json)
.map(emails_from_persistence)
.unwrap_or_default();
let phones = serde_json::from_value::<Vec<PhonePersistenceDto>>(phone_json)
.map(phones_from_persistence)
.unwrap_or_default();
let addresses = serde_json::from_value::<Vec<AddressPersistenceDto>>(address_json)
.map(addresses_from_persistence)
.unwrap_or_default();
contacts.push(Contact::from_raw(
row.get("id"),
row.get("address_book_id"),
row.get("uid"),
row.get::<Option<String>, _>("full_name"),
row.get::<Option<String>, _>("first_name"),
row.get::<Option<String>, _>("last_name"),
row.get::<Option<String>, _>("nickname"),
emails,
phones,
addresses,
row.get::<Option<String>, _>("organization"),
row.get::<Option<String>, _>("title"),
row.get::<Option<String>, _>("notes"),
row.get::<Option<String>, _>("photo_url"),
row.get("birthday"),
row.get("anniversary"),
row.get("vcard"),
row.get("etag"),
row.get("created_at"),
row.get("updated_at"),
));
}
Ok(contacts)
}
async fn get_groups_for_contact(
&self,
contact_id: &Uuid,
) -> ContactRepositoryResult<Vec<ContactGroup>> {
let rows = sqlx::query(
"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 gm ON g.id = gm.group_id WHERE gm.contact_id = $1 ORDER BY g.name"
)
.bind(contact_id)
.fetch_all(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to get groups for contact: {}", e)))?;
Ok(rows
.into_iter()
.map(|row| {
ContactGroup::from_raw(
row.get::<Uuid, _>("id"),
row.get::<Uuid, _>("address_book_id"),
row.get::<String, _>("name"),
row.get("created_at"),
row.get("updated_at"),
)
})
.collect())
}
}
@@ -1,129 +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()
}
//! 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 crate::domain::entities::contact::{Address, Email, Phone};
use serde::{Deserialize, Serialize};
/// 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,17 +1,17 @@
use async_trait::async_trait;
use chrono::Utc;
use serde_json::Value as JsonValue;
use sqlx::{PgPool, Row, types::Uuid};
use std::sync::Arc;
use serde_json::Value as JsonValue;
use super::contact_persistence_dto::{
AddressPersistenceDto, EmailPersistenceDto, PhonePersistenceDto, addresses_from_persistence,
addresses_to_persistence, emails_from_persistence, emails_to_persistence,
phones_from_persistence, phones_to_persistence,
};
use crate::common::errors::DomainError;
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,
emails_from_persistence, phones_from_persistence, addresses_from_persistence,
EmailPersistenceDto, PhonePersistenceDto, AddressPersistenceDto,
};
pub struct ContactPgRepository {
pool: Arc<PgPool>,
@@ -70,11 +70,11 @@ impl ContactRepository for ContactPgRepository {
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);
let row = sqlx::query(
r#"
INSERT INTO carddav.contacts (
@@ -90,7 +90,7 @@ impl ContactRepository for ContactPgRepository {
id, address_book_id, uid, full_name, first_name, last_name, nickname,
email, phone, address, organization, title, notes, photo_url,
birthday, anniversary, vcard, etag, created_at, updated_at
"#
"#,
)
.bind(contact.id())
.bind(contact.address_book_id())
@@ -125,15 +125,15 @@ impl ContactRepository for ContactPgRepository {
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.set_updated_at(now);
let row = sqlx::query(
r#"
UPDATE carddav.contacts
@@ -159,7 +159,7 @@ impl ContactRepository for ContactPgRepository {
id, address_book_id, uid, full_name, first_name, last_name, nickname,
email, phone, address, organization, title, notes, photo_url,
birthday, anniversary, vcard, etag, created_at, updated_at
"#
"#,
)
.bind(updated_contact.full_name_owned())
.bind(updated_contact.first_name_owned())
@@ -190,7 +190,7 @@ impl ContactRepository for ContactPgRepository {
r#"
DELETE FROM carddav.contacts
WHERE id = $1
"#
"#,
)
.bind(id)
.execute(&*self.pool)
@@ -209,7 +209,7 @@ impl ContactRepository for ContactPgRepository {
birthday, anniversary, vcard, etag, created_at, updated_at
FROM carddav.contacts
WHERE id = $1
"#
"#,
)
.bind(id)
.fetch_optional(&*self.pool)
@@ -222,7 +222,11 @@ impl ContactRepository for ContactPgRepository {
}
}
async fn get_contact_by_uid(&self, address_book_id: &Uuid, uid: &str) -> ContactRepositoryResult<Option<Contact>> {
async fn get_contact_by_uid(
&self,
address_book_id: &Uuid,
uid: &str,
) -> ContactRepositoryResult<Option<Contact>> {
let row_opt = sqlx::query(
r#"
SELECT
@@ -231,7 +235,7 @@ impl ContactRepository for ContactPgRepository {
birthday, anniversary, vcard, etag, created_at, updated_at
FROM carddav.contacts
WHERE address_book_id = $1 AND uid = $2
"#
"#,
)
.bind(address_book_id)
.bind(uid)
@@ -245,7 +249,10 @@ impl ContactRepository for ContactPgRepository {
}
}
async fn get_contacts_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>> {
async fn get_contacts_by_address_book(
&self,
address_book_id: &Uuid,
) -> ContactRepositoryResult<Vec<Contact>> {
let rows = sqlx::query(
r#"
SELECT
@@ -255,12 +262,14 @@ impl ContactRepository for ContactPgRepository {
FROM carddav.contacts
WHERE address_book_id = $1
ORDER BY full_name, first_name, last_name
"#
"#,
)
.bind(address_book_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get contacts by address book: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get contacts by address book: {}", e))
})?;
let mut contacts = Vec::new();
for row in &rows {
@@ -271,7 +280,7 @@ impl ContactRepository for ContactPgRepository {
async fn get_contacts_by_email(&self, email: &str) -> ContactRepositoryResult<Vec<Contact>> {
let search_pattern = format!("%{}%", email);
let rows = sqlx::query(
r#"
SELECT
@@ -281,12 +290,14 @@ impl ContactRepository for ContactPgRepository {
FROM carddav.contacts
WHERE email::text ILIKE $1
ORDER BY full_name, first_name, last_name
"#
"#,
)
.bind(&search_pattern)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get contacts by email: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get contacts by email: {}", e))
})?;
let mut contacts = Vec::new();
for row in &rows {
@@ -295,7 +306,10 @@ impl ContactRepository for ContactPgRepository {
Ok(contacts)
}
async fn get_contacts_by_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>> {
async fn get_contacts_by_group(
&self,
group_id: &Uuid,
) -> ContactRepositoryResult<Vec<Contact>> {
let rows = sqlx::query(
r#"
SELECT
@@ -306,12 +320,14 @@ impl ContactRepository for ContactPgRepository {
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 by group: {}", e)))?;
.map_err(|e| {
DomainError::database_error(format!("Failed to get contacts by group: {}", e))
})?;
let mut contacts = Vec::new();
for row in &rows {
@@ -320,9 +336,13 @@ impl ContactRepository for ContactPgRepository {
Ok(contacts)
}
async fn search_contacts(&self, address_book_id: &Uuid, query: &str) -> ContactRepositoryResult<Vec<Contact>> {
async fn search_contacts(
&self,
address_book_id: &Uuid,
query: &str,
) -> ContactRepositoryResult<Vec<Contact>> {
let search_pattern = format!("%{}%", query);
let rows = sqlx::query(
r#"
SELECT
@@ -341,7 +361,7 @@ impl ContactRepository for ContactPgRepository {
OR organization ILIKE $2
)
ORDER BY full_name, first_name, last_name
"#
"#,
)
.bind(address_book_id)
.bind(&search_pattern)
@@ -355,4 +375,4 @@ impl ContactRepository for ContactPgRepository {
}
Ok(contacts)
}
}
}
@@ -1,12 +1,12 @@
use std::sync::Arc;
use async_trait::async_trait;
use sqlx::{PgPool, Row};
use std::sync::Arc;
use tracing::error;
use uuid::Uuid;
use crate::application::dtos::favorites_dto::FavoriteItemDto;
use crate::application::ports::favorites_ports::FavoritesRepositoryPort;
use crate::common::errors::{Result, DomainError, ErrorKind};
use crate::common::errors::{DomainError, ErrorKind, Result};
/// PostgreSQL implementation of the favorites persistence port.
pub struct FavoritesPgRepository {
@@ -42,7 +42,11 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
.await
.map_err(|e| {
error!("Database error fetching favorites: {}", e);
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to fetch favorites: {}", e))
DomainError::new(
ErrorKind::InternalError,
"Favorites",
format!("Failed to fetch favorites: {}", e),
)
})?;
let favorites = rows
@@ -76,7 +80,11 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
.await
.map_err(|e| {
error!("Database error adding favorite: {}", e);
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to add to favorites: {}", e))
DomainError::new(
ErrorKind::InternalError,
"Favorites",
format!("Failed to add to favorites: {}", e),
)
})?;
Ok(())
@@ -98,7 +106,11 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
.await
.map_err(|e| {
error!("Database error removing favorite: {}", e);
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to remove from favorites: {}", e))
DomainError::new(
ErrorKind::InternalError,
"Favorites",
format!("Failed to remove from favorites: {}", e),
)
})?;
Ok(result.rows_affected() > 0)
@@ -122,7 +134,11 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
.await
.map_err(|e| {
error!("Database error checking favorite status: {}", e);
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to check favorite status: {}", e))
DomainError::new(
ErrorKind::InternalError,
"Favorites",
format!("Failed to check favorite status: {}", e),
)
})?;
Ok(row.try_get("is_favorite").unwrap_or(false))
+4 -4
View File
@@ -1,9 +1,9 @@
mod address_book_pg_repository;
mod calendar_pg_repository;
mod calendar_event_pg_repository;
mod contact_pg_repository;
mod calendar_pg_repository;
mod contact_group_pg_repository;
mod contact_persistence_dto;
mod contact_pg_repository;
mod favorites_pg_repository;
mod recent_items_pg_repository;
mod session_pg_repository;
@@ -12,11 +12,11 @@ mod transaction_utils;
mod user_pg_repository;
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 calendar_pg_repository::CalendarPgRepository;
pub use contact_group_pg_repository::ContactGroupPgRepository;
pub use contact_persistence_dto::*;
pub use contact_pg_repository::ContactPgRepository;
pub use favorites_pg_repository::FavoritesPgRepository;
pub use recent_items_pg_repository::RecentItemsPgRepository;
pub use session_pg_repository::SessionPgRepository;
@@ -1,12 +1,12 @@
use std::sync::Arc;
use async_trait::async_trait;
use sqlx::{PgPool, Row};
use std::sync::Arc;
use tracing::error;
use uuid::Uuid;
use crate::application::dtos::recent_dto::RecentItemDto;
use crate::application::ports::recent_ports::RecentItemsRepositoryPort;
use crate::common::errors::{Result, DomainError, ErrorKind};
use crate::common::errors::{DomainError, ErrorKind, Result};
/// PostgreSQL implementation of the recent items persistence port.
pub struct RecentItemsPgRepository {
@@ -44,7 +44,11 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
.await
.map_err(|e| {
error!("Database error fetching recent items: {}", e);
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to fetch recent items: {}", e))
DomainError::new(
ErrorKind::InternalError,
"RecentItems",
format!("Failed to fetch recent items: {}", e),
)
})?;
let items = rows
@@ -79,7 +83,11 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
.await
.map_err(|e| {
error!("Database error upserting recent item access: {}", e);
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to record item access: {}", e))
DomainError::new(
ErrorKind::InternalError,
"RecentItems",
format!("Failed to record item access: {}", e),
)
})?;
Ok(())
@@ -101,7 +109,11 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
.await
.map_err(|e| {
error!("Database error removing recent item: {}", e);
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to remove recent item: {}", e))
DomainError::new(
ErrorKind::InternalError,
"RecentItems",
format!("Failed to remove recent item: {}", e),
)
})?;
Ok(result.rows_affected() > 0)
@@ -121,7 +133,11 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
.await
.map_err(|e| {
error!("Database error clearing recent items: {}", e);
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to clear recent items: {}", e))
DomainError::new(
ErrorKind::InternalError,
"RecentItems",
format!("Failed to clear recent items: {}", e),
)
})?;
Ok(())
@@ -147,7 +163,11 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
.await
.map_err(|e| {
error!("Database error pruning old recent items: {}", e);
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to prune recent items: {}", e))
DomainError::new(
ErrorKind::InternalError,
"RecentItems",
format!("Failed to prune recent items: {}", e),
)
})?;
Ok(())
@@ -1,13 +1,15 @@
use async_trait::async_trait;
use sqlx::{PgPool, Row};
use std::sync::Arc;
use chrono::Utc;
use futures::future::BoxFuture;
use sqlx::{PgPool, Row};
use std::sync::Arc;
use crate::domain::entities::session::Session;
use crate::domain::repositories::session_repository::{SessionRepository, SessionRepositoryError, SessionRepositoryResult};
use crate::application::ports::auth_ports::SessionStoragePort;
use crate::common::errors::DomainError;
use crate::domain::entities::session::Session;
use crate::domain::repositories::session_repository::{
SessionRepository, SessionRepositoryError, SessionRepositoryResult,
};
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
// Implement From<sqlx::Error> for SessionRepositoryError to allow automatic conversions
@@ -25,16 +27,14 @@ impl SessionPgRepository {
pub fn new(pool: Arc<PgPool>) -> Self {
Self { pool }
}
// Helper method to map SQL errors to domain errors
pub fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
match err {
sqlx::Error::RowNotFound => {
SessionRepositoryError::NotFound("Session not found".to_string())
},
_ => SessionRepositoryError::DatabaseError(
format!("Database error: {}", err)
),
}
_ => SessionRepositoryError::DatabaseError(format!("Database error: {}", err)),
}
}
}
@@ -45,65 +45,66 @@ impl SessionRepository for SessionPgRepository {
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session> {
// Create a copy of the session for the closure
let session_clone = session.clone();
with_transaction(
&self.pool,
"create_session",
|tx| {
Box::pin(async move {
// Insert the session
sqlx::query(
r#"
with_transaction(&self.pool, "create_session", |tx| {
Box::pin(async move {
// Insert the session
sqlx::query(
r#"
INSERT INTO auth.sessions (
id, user_id, refresh_token, expires_at,
ip_address, user_agent, created_at, revoked
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
"#
)
.bind(session_clone.id())
.bind(session_clone.user_id())
.bind(session_clone.refresh_token())
.bind(session_clone.expires_at())
.bind(session_clone.ip_address())
.bind(session_clone.user_agent())
.bind(session_clone.created_at())
.bind(session_clone.is_revoked())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// Optionally, update the user's last login
// within the same transaction
sqlx::query(
r#"
"#,
)
.bind(session_clone.id())
.bind(session_clone.user_id())
.bind(session_clone.refresh_token())
.bind(session_clone.expires_at())
.bind(session_clone.ip_address())
.bind(session_clone.user_agent())
.bind(session_clone.created_at())
.bind(session_clone.is_revoked())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// Optionally, update the user's last login
// within the same transaction
sqlx::query(
r#"
UPDATE auth.users
SET last_login_at = NOW(), updated_at = NOW()
WHERE id = $1
"#
)
.bind(session_clone.user_id())
.execute(&mut **tx)
.await
.map_err(|e| {
// Convert the error but without interrupting session
// creation if the update fails
tracing::warn!("Could not update last_login_at for user {}: {}",
session_clone.user_id(), e);
SessionRepositoryError::DatabaseError(format!(
"Session created but could not update last_login_at: {}", e
))
})?;
Ok(session_clone)
}) as BoxFuture<'_, SessionRepositoryResult<Session>>
}
).await?;
"#,
)
.bind(session_clone.user_id())
.execute(&mut **tx)
.await
.map_err(|e| {
// Convert the error but without interrupting session
// creation if the update fails
tracing::warn!(
"Could not update last_login_at for user {}: {}",
session_clone.user_id(),
e
);
SessionRepositoryError::DatabaseError(format!(
"Session created but could not update last_login_at: {}",
e
))
})?;
Ok(session_clone)
}) as BoxFuture<'_, SessionRepositoryResult<Session>>
})
.await?;
Ok(session)
}
/// Gets a session by ID
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session> {
let row = sqlx::query(
@@ -113,7 +114,7 @@ impl SessionRepository for SessionPgRepository {
ip_address, user_agent, created_at, revoked
FROM auth.sessions
WHERE id = $1
"#
"#,
)
.bind(id)
.fetch_one(&*self.pool)
@@ -131,9 +132,12 @@ impl SessionRepository for SessionPgRepository {
row.get("revoked"),
))
}
/// Gets a session by refresh token
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult<Session> {
async fn get_session_by_refresh_token(
&self,
refresh_token: &str,
) -> SessionRepositoryResult<Session> {
let row = sqlx::query(
r#"
SELECT
@@ -141,7 +145,7 @@ impl SessionRepository for SessionPgRepository {
ip_address, user_agent, created_at, revoked
FROM auth.sessions
WHERE refresh_token = $1
"#
"#,
)
.bind(refresh_token)
.fetch_one(&*self.pool)
@@ -159,9 +163,12 @@ impl SessionRepository for SessionPgRepository {
row.get("revoked"),
))
}
/// Gets all sessions for a user
async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>> {
async fn get_sessions_by_user_id(
&self,
user_id: &str,
) -> SessionRepositoryResult<Vec<Session>> {
let rows = sqlx::query(
r#"
SELECT
@@ -170,14 +177,15 @@ impl SessionRepository for SessionPgRepository {
FROM auth.sessions
WHERE user_id = $1
ORDER BY created_at DESC
"#
"#,
)
.bind(user_id)
.fetch_all(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
let sessions = rows.into_iter()
let sessions = rows
.into_iter()
.map(|row| {
Session::from_raw(
row.get("id"),
@@ -194,90 +202,84 @@ impl SessionRepository for SessionPgRepository {
Ok(sessions)
}
/// Revokes a specific session using a transaction
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> {
let id = session_id.to_string(); // Clone for use in closure
with_transaction(
&self.pool,
"revoke_session",
|tx| {
Box::pin(async move {
// Revoke the session
let result = sqlx::query(
r#"
with_transaction(&self.pool, "revoke_session", |tx| {
Box::pin(async move {
// Revoke the session
let result = sqlx::query(
r#"
UPDATE auth.sessions
SET revoked = true
WHERE id = $1
RETURNING user_id
"#
)
.bind(&id)
.fetch_optional(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// If we found the session, we can log a security event
if let Some(row) = result {
let user_id: String = row.try_get("user_id").unwrap_or_default();
// Log security event (in a security table)
// This is optional but shows how additional operations
// can be performed in the same transaction
tracing::info!("Session with ID {} for user {} revoked", id, user_id);
}
Ok(())
}) as BoxFuture<'_, SessionRepositoryResult<()>>
}
).await
"#,
)
.bind(&id)
.fetch_optional(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// If we found the session, we can log a security event
if let Some(row) = result {
let user_id: String = row.try_get("user_id").unwrap_or_default();
// Log security event (in a security table)
// This is optional but shows how additional operations
// can be performed in the same transaction
tracing::info!("Session with ID {} for user {} revoked", id, user_id);
}
Ok(())
}) as BoxFuture<'_, SessionRepositoryResult<()>>
})
.await
}
/// Revokes all sessions for a user using a transaction
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64> {
let user_id_clone = user_id.to_string(); // Clone for use in closure
with_transaction(
&self.pool,
"revoke_all_user_sessions",
|tx| {
Box::pin(async move {
// Revoke all sessions for the user
let result = sqlx::query(
r#"
with_transaction(&self.pool, "revoke_all_user_sessions", |tx| {
Box::pin(async move {
// Revoke all sessions for the user
let result = sqlx::query(
r#"
UPDATE auth.sessions
SET revoked = true
WHERE user_id = $1 AND revoked = false
"#
)
.bind(&user_id_clone)
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
let affected = result.rows_affected();
// Log security event
if affected > 0 {
tracing::info!("Revoked {} sessions for user {}", affected, user_id_clone);
}
Ok(affected)
}) as BoxFuture<'_, SessionRepositoryResult<u64>>
}
).await
"#,
)
.bind(&user_id_clone)
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
let affected = result.rows_affected();
// Log security event
if affected > 0 {
tracing::info!("Revoked {} sessions for user {}", affected, user_id_clone);
}
Ok(affected)
}) as BoxFuture<'_, SessionRepositoryResult<u64>>
})
.await
}
/// Deletes expired sessions
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64> {
let now = Utc::now();
let result = sqlx::query(
r#"
DELETE FROM auth.sessions
WHERE expires_at < $1
"#
"#,
)
.bind(now)
.execute(&*self.pool)
@@ -292,22 +294,29 @@ impl SessionRepository for SessionPgRepository {
#[async_trait]
impl SessionStoragePort for SessionPgRepository {
async fn create_session(&self, session: Session) -> Result<Session, DomainError> {
SessionRepository::create_session(self, session).await.map_err(DomainError::from)
SessionRepository::create_session(self, session)
.await
.map_err(DomainError::from)
}
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> Result<Session, DomainError> {
async fn get_session_by_refresh_token(
&self,
refresh_token: &str,
) -> Result<Session, DomainError> {
SessionRepository::get_session_by_refresh_token(self, refresh_token)
.await
.map_err(DomainError::from)
}
async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError> {
SessionRepository::revoke_session(self, session_id).await.map_err(DomainError::from)
SessionRepository::revoke_session(self, session_id)
.await
.map_err(DomainError::from)
}
async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError> {
SessionRepository::revoke_all_user_sessions(self, user_id)
.await
.map_err(DomainError::from)
}
}
}
@@ -1,88 +1,102 @@
use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use sqlx::PgPool;
use crate::domain::repositories::settings_repository::SettingsRepository;
use crate::common::errors::{DomainError, ErrorKind};
pub struct SettingsPgRepository {
pool: Arc<PgPool>,
}
impl SettingsPgRepository {
pub fn new(pool: Arc<PgPool>) -> Self {
Self { pool }
}
}
#[async_trait]
impl SettingsRepository for SettingsPgRepository {
async fn get(&self, key: &str) -> Result<Option<String>, DomainError> {
let row = sqlx::query_scalar::<_, String>(
"SELECT value FROM auth.admin_settings WHERE key = $1"
)
.bind(key)
.fetch_optional(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(
ErrorKind::InternalError, "Settings", format!("DB error: {}", e),
))?;
Ok(row)
}
async fn get_by_category(&self, category: &str) -> Result<HashMap<String, String>, DomainError> {
let rows = sqlx::query_as::<_, (String, String)>(
"SELECT key, value FROM auth.admin_settings WHERE category = $1"
)
.bind(category)
.fetch_all(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(
ErrorKind::InternalError, "Settings", format!("DB error: {}", e),
))?;
Ok(rows.into_iter().collect())
}
async fn set(
&self,
key: &str,
value: &str,
category: &str,
is_secret: bool,
updated_by: Option<&str>,
) -> Result<(), DomainError> {
sqlx::query(
"INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW())
ON CONFLICT (key) DO UPDATE
SET value = $2, category = $3, is_secret = $4, updated_by = $5, updated_at = NOW()"
)
.bind(key)
.bind(value)
.bind(category)
.bind(is_secret)
.bind(updated_by)
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(
ErrorKind::InternalError, "Settings", format!("DB error: {}", e),
))?;
Ok(())
}
async fn delete(&self, key: &str) -> Result<(), DomainError> {
sqlx::query("DELETE FROM auth.admin_settings WHERE key = $1")
.bind(key)
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(
ErrorKind::InternalError, "Settings", format!("DB error: {}", e),
))?;
Ok(())
}
}
use async_trait::async_trait;
use sqlx::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use crate::common::errors::{DomainError, ErrorKind};
use crate::domain::repositories::settings_repository::SettingsRepository;
pub struct SettingsPgRepository {
pool: Arc<PgPool>,
}
impl SettingsPgRepository {
pub fn new(pool: Arc<PgPool>) -> Self {
Self { pool }
}
}
#[async_trait]
impl SettingsRepository for SettingsPgRepository {
async fn get(&self, key: &str) -> Result<Option<String>, DomainError> {
let row =
sqlx::query_scalar::<_, String>("SELECT value FROM auth.admin_settings WHERE key = $1")
.bind(key)
.fetch_optional(self.pool.as_ref())
.await
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"Settings",
format!("DB error: {}", e),
)
})?;
Ok(row)
}
async fn get_by_category(
&self,
category: &str,
) -> Result<HashMap<String, String>, DomainError> {
let rows = sqlx::query_as::<_, (String, String)>(
"SELECT key, value FROM auth.admin_settings WHERE category = $1",
)
.bind(category)
.fetch_all(self.pool.as_ref())
.await
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"Settings",
format!("DB error: {}", e),
)
})?;
Ok(rows.into_iter().collect())
}
async fn set(
&self,
key: &str,
value: &str,
category: &str,
is_secret: bool,
updated_by: Option<&str>,
) -> Result<(), DomainError> {
sqlx::query(
"INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW())
ON CONFLICT (key) DO UPDATE
SET value = $2, category = $3, is_secret = $4, updated_by = $5, updated_at = NOW()"
)
.bind(key)
.bind(value)
.bind(category)
.bind(is_secret)
.bind(updated_by)
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::new(
ErrorKind::InternalError, "Settings", format!("DB error: {}", e),
))?;
Ok(())
}
async fn delete(&self, key: &str) -> Result<(), DomainError> {
sqlx::query("DELETE FROM auth.admin_settings WHERE key = $1")
.bind(key)
.execute(self.pool.as_ref())
.await
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"Settings",
format!("DB error: {}", e),
)
})?;
Ok(())
}
}
@@ -1,4 +1,4 @@
use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError};
use sqlx::{Error as SqlxError, PgPool, Postgres, Transaction};
use std::sync::Arc;
use tracing::{debug, error, info};
@@ -13,17 +13,19 @@ pub async fn with_transaction<F, T, E>(
operation: F,
) -> Result<T, E>
where
F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result<T, E>>,
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 for: {}", operation_name);
// Begin transaction
let mut tx = pool.begin().await.map_err(|e| {
error!("Failed to begin transaction for {}: {}", operation_name, e);
E::from(e)
})?;
// Execute the operation within the transaction
match operation(&mut tx).await {
Ok(result) => {
@@ -32,17 +34,20 @@ where
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);
error!(
"Failed to rollback transaction for {}: {}",
operation_name, rollback_err
);
// Still return the original error
} else {
info!("Transaction rolled back for {}: {}", operation_name, e);
@@ -50,4 +55,4 @@ where
Err(e)
}
}
}
}
@@ -1,12 +1,14 @@
use async_trait::async_trait;
use futures::future::BoxFuture;
use sqlx::{PgPool, Row};
use std::sync::Arc;
use futures::future::BoxFuture;
use crate::domain::entities::user::{User, UserRole};
use crate::domain::repositories::user_repository::{UserRepository, UserRepositoryError, UserRepositoryResult, StorageStats};
use crate::application::ports::auth_ports::UserStoragePort;
use crate::common::errors::DomainError;
use crate::domain::entities::user::{User, UserRole};
use crate::domain::repositories::user_repository::{
StorageStats, UserRepository, UserRepositoryError, UserRepositoryResult,
};
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
// Implement From<sqlx::Error> for UserRepositoryError to allow automatic conversions
@@ -24,28 +26,20 @@ impl UserPgRepository {
pub fn new(pool: Arc<PgPool>) -> Self {
Self { pool }
}
// Helper method to map SQL errors to domain errors
pub fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError {
match err {
sqlx::Error::RowNotFound => {
UserRepositoryError::NotFound("User not found".to_string())
},
sqlx::Error::RowNotFound => UserRepositoryError::NotFound("User not found".to_string()),
sqlx::Error::Database(db_err) => {
if db_err.code().is_some_and(|code| code == "23505") {
// PostgreSQL uniqueness violation code
UserRepositoryError::AlreadyExists(
"User or email already exists".to_string()
)
UserRepositoryError::AlreadyExists("User or email already exists".to_string())
} else {
UserRepositoryError::DatabaseError(
format!("Database error: {}", db_err)
)
UserRepositoryError::DatabaseError(format!("Database error: {}", db_err))
}
},
_ => UserRepositoryError::DatabaseError(
format!("Database error: {}", err)
),
}
_ => UserRepositoryError::DatabaseError(format!("Database error: {}", err)),
}
}
}
@@ -56,21 +50,18 @@ impl UserRepository for UserPgRepository {
async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
// Create a copy of the user for the closure
let user_clone = user.clone();
with_transaction(
&self.pool,
"create_user",
|tx| {
// We need to move the closure into a BoxFuture to return inside
// the with_transaction call
Box::pin(async move {
// Use getters to extract the values
// Convert user.role() to string to pass it as plain text
let role_str = user_clone.role().to_string();
// Modify the SQL to do an explicit cast to the auth.userrole type
let _result = sqlx::query(
r#"
with_transaction(&self.pool, "create_user", |tx| {
// We need to move the closure into a BoxFuture to return inside
// the with_transaction call
Box::pin(async move {
// Use getters to extract the values
// Convert user.role() to string to pass it as plain text
let role_str = user_clone.role().to_string();
// Modify the SQL to do an explicit cast to the auth.userrole type
let _result = sqlx::query(
r#"
INSERT INTO auth.users (
id, username, email, password_hash, role,
storage_quota_bytes, storage_used_bytes,
@@ -81,36 +72,36 @@ impl UserRepository for UserPgRepository {
$12, $13
)
RETURNING *
"#
)
.bind(user_clone.id())
.bind(user_clone.username())
.bind(user_clone.email())
.bind(user_clone.password_hash())
.bind(&role_str) // Convert to string but with explicit cast in SQL
.bind(user_clone.storage_quota_bytes())
.bind(user_clone.storage_used_bytes())
.bind(user_clone.created_at())
.bind(user_clone.updated_at())
.bind(user_clone.last_login_at())
.bind(user_clone.is_active())
.bind(user_clone.oidc_provider())
.bind(user_clone.oidc_subject())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// We could perform additional operations here,
// such as configuring permissions, roles, etc.
Ok(user_clone)
}) as BoxFuture<'_, UserRepositoryResult<User>>
}
).await?;
"#,
)
.bind(user_clone.id())
.bind(user_clone.username())
.bind(user_clone.email())
.bind(user_clone.password_hash())
.bind(&role_str) // Convert to string but with explicit cast in SQL
.bind(user_clone.storage_quota_bytes())
.bind(user_clone.storage_used_bytes())
.bind(user_clone.created_at())
.bind(user_clone.updated_at())
.bind(user_clone.last_login_at())
.bind(user_clone.is_active())
.bind(user_clone.oidc_provider())
.bind(user_clone.oidc_subject())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// We could perform additional operations here,
// such as configuring permissions, roles, etc.
Ok(user_clone)
}) as BoxFuture<'_, UserRepositoryResult<User>>
})
.await?;
Ok(user) // Return the original user for simplicity
}
/// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User> {
let row = sqlx::query(
@@ -122,7 +113,7 @@ impl UserRepository for UserPgRepository {
oidc_provider, oidc_subject
FROM auth.users
WHERE id = $1
"#
"#,
)
.bind(id)
.fetch_one(&*self.pool)
@@ -135,7 +126,7 @@ impl UserRepository for UserPgRepository {
Some("admin") => UserRole::Admin,
_ => UserRole::User,
};
Ok(User::from_data_full(
row.get("id"),
row.get("username"),
@@ -152,7 +143,7 @@ impl UserRepository for UserPgRepository {
row.get("oidc_subject"),
))
}
/// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User> {
let row = sqlx::query(
@@ -164,7 +155,7 @@ impl UserRepository for UserPgRepository {
oidc_provider, oidc_subject
FROM auth.users
WHERE username = $1
"#
"#,
)
.bind(username)
.fetch_one(&*self.pool)
@@ -177,7 +168,7 @@ impl UserRepository for UserPgRepository {
Some("admin") => UserRole::Admin,
_ => UserRole::User,
};
Ok(User::from_data_full(
row.get("id"),
row.get("username"),
@@ -194,7 +185,7 @@ impl UserRepository for UserPgRepository {
row.get("oidc_subject"),
))
}
/// Gets a user by email
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User> {
let row = sqlx::query(
@@ -206,7 +197,7 @@ impl UserRepository for UserPgRepository {
oidc_provider, oidc_subject
FROM auth.users
WHERE email = $1
"#
"#,
)
.bind(email)
.fetch_one(&*self.pool)
@@ -219,7 +210,7 @@ impl UserRepository for UserPgRepository {
Some("admin") => UserRole::Admin,
_ => UserRole::User,
};
Ok(User::from_data_full(
row.get("id"),
row.get("username"),
@@ -236,20 +227,17 @@ impl UserRepository for UserPgRepository {
row.get("oidc_subject"),
))
}
/// Updates an existing user using a transaction
async fn update_user(&self, user: User) -> UserRepositoryResult<User> {
// Create a copy of the user for the closure
let user_clone = user.clone();
with_transaction(
&self.pool,
"update_user",
|tx| {
Box::pin(async move {
// Update the user
sqlx::query(
r#"
with_transaction(&self.pool, "update_user", |tx| {
Box::pin(async move {
// Update the user
sqlx::query(
r#"
UPDATE auth.users
SET
username = $2,
@@ -262,35 +250,39 @@ impl UserRepository for UserPgRepository {
last_login_at = $9,
active = $10
WHERE id = $1
"#
)
.bind(user_clone.id())
.bind(user_clone.username())
.bind(user_clone.email())
.bind(user_clone.password_hash())
.bind(user_clone.role().to_string())
.bind(user_clone.storage_quota_bytes())
.bind(user_clone.storage_used_bytes())
.bind(user_clone.updated_at())
.bind(user_clone.last_login_at())
.bind(user_clone.is_active())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// We could perform additional operations here inside
// the same transaction, such as updating permissions, etc.
Ok(user_clone)
}) as BoxFuture<'_, UserRepositoryResult<User>>
}
).await?;
"#,
)
.bind(user_clone.id())
.bind(user_clone.username())
.bind(user_clone.email())
.bind(user_clone.password_hash())
.bind(user_clone.role().to_string())
.bind(user_clone.storage_quota_bytes())
.bind(user_clone.storage_used_bytes())
.bind(user_clone.updated_at())
.bind(user_clone.last_login_at())
.bind(user_clone.is_active())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// We could perform additional operations here inside
// the same transaction, such as updating permissions, etc.
Ok(user_clone)
}) as BoxFuture<'_, UserRepositoryResult<User>>
})
.await?;
Ok(user)
}
/// Updates only the storage usage of a user
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()> {
async fn update_storage_usage(
&self,
user_id: &str,
usage_bytes: i64,
) -> UserRepositoryResult<()> {
sqlx::query(
r#"
UPDATE auth.users
@@ -298,7 +290,7 @@ impl UserRepository for UserPgRepository {
storage_used_bytes = $2,
updated_at = NOW()
WHERE id = $1
"#
"#,
)
.bind(user_id)
.bind(usage_bytes)
@@ -308,7 +300,7 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Updates the last login date
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()> {
sqlx::query(
@@ -318,7 +310,7 @@ impl UserRepository for UserPgRepository {
last_login_at = NOW(),
updated_at = NOW()
WHERE id = $1
"#
"#,
)
.bind(user_id)
.execute(&*self.pool)
@@ -327,7 +319,7 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Lists users with pagination
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>> {
let rows = sqlx::query(
@@ -340,7 +332,7 @@ impl UserRepository for UserPgRepository {
FROM auth.users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
"#
"#,
)
.bind(limit)
.bind(offset)
@@ -348,7 +340,8 @@ impl UserRepository for UserPgRepository {
.await
.map_err(Self::map_sqlx_error)?;
let users = rows.into_iter()
let users = rows
.into_iter()
.map(|row| {
// Convert role string to UserRole enum for each row
let role_str: Option<String> = row.try_get("role_text").unwrap_or(None);
@@ -356,7 +349,7 @@ impl UserRepository for UserPgRepository {
Some("admin") => UserRole::Admin,
_ => UserRole::User,
};
User::from_data_full(
row.get("id"),
row.get("username"),
@@ -377,9 +370,13 @@ impl UserRepository for UserPgRepository {
Ok(users)
}
/// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()> {
async fn set_user_active_status(
&self,
user_id: &str,
active: bool,
) -> UserRepositoryResult<()> {
sqlx::query(
r#"
UPDATE auth.users
@@ -387,7 +384,7 @@ impl UserRepository for UserPgRepository {
active = $2,
updated_at = NOW()
WHERE id = $1
"#
"#,
)
.bind(user_id)
.bind(active)
@@ -397,9 +394,13 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Changes a user's password
async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()> {
async fn change_password(
&self,
user_id: &str,
password_hash: &str,
) -> UserRepositoryResult<()> {
sqlx::query(
r#"
UPDATE auth.users
@@ -407,7 +408,7 @@ impl UserRepository for UserPgRepository {
password_hash = $2,
updated_at = NOW()
WHERE id = $1
"#
"#,
)
.bind(user_id)
.bind(password_hash)
@@ -417,12 +418,12 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Changes a user's role
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()> {
// Convert the role to string for the binding
let role_str = role.to_string();
sqlx::query(
r#"
UPDATE auth.users
@@ -430,7 +431,7 @@ impl UserRepository for UserPgRepository {
role = $2::auth.userrole,
updated_at = NOW()
WHERE id = $1
"#
"#,
)
.bind(user_id)
.bind(&role_str)
@@ -440,7 +441,7 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Lists users by role
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>> {
let rows = sqlx::query(
@@ -453,14 +454,15 @@ impl UserRepository for UserPgRepository {
FROM auth.users
WHERE role::text = $1
ORDER BY created_at DESC
"#
"#,
)
.bind(role)
.fetch_all(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
let users = rows.into_iter()
let users = rows
.into_iter()
.map(|row| {
// Convert role string to UserRole enum for each row
let role_str: Option<String> = row.try_get("role_text").unwrap_or(None);
@@ -468,7 +470,7 @@ impl UserRepository for UserPgRepository {
Some("admin") => UserRole::Admin,
_ => UserRole::User,
};
User::from_data_full(
row.get("id"),
row.get("username"),
@@ -489,14 +491,14 @@ impl UserRepository for UserPgRepository {
Ok(users)
}
/// Deletes a user
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> {
sqlx::query(
r#"
DELETE FROM auth.users
WHERE id = $1
"#
"#,
)
.bind(user_id)
.execute(&*self.pool)
@@ -507,7 +509,11 @@ impl UserRepository for UserPgRepository {
}
/// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> UserRepositoryResult<User> {
async fn get_user_by_oidc_subject(
&self,
provider: &str,
subject: &str,
) -> UserRepositoryResult<User> {
let row = sqlx::query(
r#"
SELECT
@@ -517,7 +523,7 @@ impl UserRepository for UserPgRepository {
oidc_provider, oidc_subject
FROM auth.users
WHERE oidc_provider = $1 AND oidc_subject = $2
"#
"#,
)
.bind(provider)
.bind(subject)
@@ -549,7 +555,11 @@ impl UserRepository for UserPgRepository {
}
/// Updates a user's storage quota
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()> {
async fn update_storage_quota(
&self,
user_id: &str,
quota_bytes: i64,
) -> UserRepositoryResult<()> {
sqlx::query(
r#"
UPDATE auth.users
@@ -557,7 +567,7 @@ impl UserRepository for UserPgRepository {
storage_quota_bytes = $2,
updated_at = NOW()
WHERE id = $1
"#
"#,
)
.bind(user_id)
.bind(quota_bytes)
@@ -570,12 +580,10 @@ impl UserRepository for UserPgRepository {
/// Counts the total number of users
async fn count_users(&self) -> UserRepositoryResult<i64> {
let row = sqlx::query(
"SELECT COUNT(*) as count FROM auth.users"
)
.fetch_one(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
let row = sqlx::query("SELECT COUNT(*) as count FROM auth.users")
.fetch_one(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
let count: i64 = row.get("count");
Ok(count)
@@ -614,52 +622,74 @@ impl UserRepository for UserPgRepository {
#[async_trait]
impl UserStoragePort for UserPgRepository {
async fn create_user(&self, user: User) -> Result<User, DomainError> {
UserRepository::create_user(self, user).await.map_err(DomainError::from)
UserRepository::create_user(self, user)
.await
.map_err(DomainError::from)
}
async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError> {
UserRepository::get_user_by_id(self, id).await.map_err(DomainError::from)
UserRepository::get_user_by_id(self, id)
.await
.map_err(DomainError::from)
}
async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError> {
UserRepository::get_user_by_username(self, username).await.map_err(DomainError::from)
UserRepository::get_user_by_username(self, username)
.await
.map_err(DomainError::from)
}
async fn get_user_by_email(&self, email: &str) -> Result<User, DomainError> {
UserRepository::get_user_by_email(self, email).await.map_err(DomainError::from)
UserRepository::get_user_by_email(self, email)
.await
.map_err(DomainError::from)
}
async fn update_user(&self, user: User) -> Result<User, DomainError> {
UserRepository::update_user(self, user).await.map_err(DomainError::from)
UserRepository::update_user(self, user)
.await
.map_err(DomainError::from)
}
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> Result<(), DomainError> {
async fn update_storage_usage(
&self,
user_id: &str,
usage_bytes: i64,
) -> Result<(), DomainError> {
UserRepository::update_storage_usage(self, user_id, usage_bytes)
.await
.map_err(DomainError::from)
}
async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<User>, DomainError> {
UserRepository::list_users(self, limit, offset).await.map_err(DomainError::from)
UserRepository::list_users(self, limit, offset)
.await
.map_err(DomainError::from)
}
async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError> {
UserRepository::list_users_by_role(self, role).await.map_err(DomainError::from)
UserRepository::list_users_by_role(self, role)
.await
.map_err(DomainError::from)
}
async fn delete_user(&self, user_id: &str) -> Result<(), DomainError> {
UserRepository::delete_user(self, user_id)
.await
.map_err(DomainError::from)
}
async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError> {
UserRepository::change_password(self, user_id, password_hash)
.await
.map_err(DomainError::from)
}
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> Result<User, DomainError> {
async fn get_user_by_oidc_subject(
&self,
provider: &str,
subject: &str,
) -> Result<User, DomainError> {
UserRepository::get_user_by_oidc_subject(self, provider, subject)
.await
.map_err(DomainError::from)
@@ -681,7 +711,11 @@ impl UserStoragePort for UserPgRepository {
.map_err(DomainError::from)
}
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> Result<(), DomainError> {
async fn update_storage_quota(
&self,
user_id: &str,
quota_bytes: i64,
) -> Result<(), DomainError> {
UserRepository::update_storage_quota(self, user_id, quota_bytes)
.await
.map_err(DomainError::from)
@@ -692,4 +726,4 @@ impl UserStoragePort for UserPgRepository {
.await
.map_err(DomainError::from)
}
}
}
@@ -12,22 +12,22 @@ use crate::common::errors::DomainError;
pub enum FileRepositoryError {
#[error("File not found: {0}")]
NotFound(String),
#[error("File already exists: {0}")]
AlreadyExists(String),
#[error("Invalid file path: {0}")]
InvalidPath(String),
#[error("Operation not supported: {0}")]
OperationNotSupported(String),
#[error("Storage error: {0}")]
StorageError(String),
#[error("Domain error: {0}")]
DomainError(#[from] DomainError),
#[error("Other error: {0}")]
Other(String),
}
@@ -39,25 +39,25 @@ pub type FileRepositoryResult<T> = Result<T, FileRepositoryError>;
pub enum FolderRepositoryError {
#[error("Folder not found: {0}")]
NotFound(String),
#[error("Folder already exists: {0}")]
AlreadyExists(String),
#[error("Invalid folder path: {0}")]
InvalidPath(String),
#[error("Operation not supported: {0}")]
OperationNotSupported(String),
#[error("Storage error: {0}")]
StorageError(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Domain error: {0}")]
DomainError(#[from] DomainError),
#[error("Other error: {0}")]
Other(String),
}
@@ -71,10 +71,16 @@ impl From<FileRepositoryError> for DomainError {
match err {
FileRepositoryError::NotFound(id) => DomainError::not_found("File", id),
FileRepositoryError::AlreadyExists(path) => DomainError::already_exists("File", path),
FileRepositoryError::InvalidPath(path) => DomainError::validation_error(format!("Invalid path: {}", path)),
FileRepositoryError::StorageError(msg) => DomainError::internal_error("File", format!("Storage error: {}", msg)),
FileRepositoryError::InvalidPath(path) => {
DomainError::validation_error(format!("Invalid path: {}", path))
}
FileRepositoryError::StorageError(msg) => {
DomainError::internal_error("File", format!("Storage error: {}", msg))
}
FileRepositoryError::Other(msg) => DomainError::internal_error("File", msg),
FileRepositoryError::OperationNotSupported(msg) => DomainError::operation_not_supported("File", msg),
FileRepositoryError::OperationNotSupported(msg) => {
DomainError::operation_not_supported("File", msg)
}
FileRepositoryError::DomainError(e) => e,
}
}
@@ -84,12 +90,20 @@ impl From<FolderRepositoryError> for DomainError {
fn from(err: FolderRepositoryError) -> Self {
match err {
FolderRepositoryError::NotFound(id) => DomainError::not_found("Folder", id),
FolderRepositoryError::AlreadyExists(path) => DomainError::already_exists("Folder", path),
FolderRepositoryError::InvalidPath(path) => DomainError::validation_error(format!("Invalid path: {}", path)),
FolderRepositoryError::StorageError(msg) => DomainError::internal_error("Folder", format!("Storage error: {}", msg)),
FolderRepositoryError::AlreadyExists(path) => {
DomainError::already_exists("Folder", path)
}
FolderRepositoryError::InvalidPath(path) => {
DomainError::validation_error(format!("Invalid path: {}", path))
}
FolderRepositoryError::StorageError(msg) => {
DomainError::internal_error("Folder", format!("Storage error: {}", msg))
}
FolderRepositoryError::ValidationError(msg) => DomainError::validation_error(msg),
FolderRepositoryError::Other(msg) => DomainError::internal_error("Folder", msg),
FolderRepositoryError::OperationNotSupported(msg) => DomainError::operation_not_supported("Folder", msg),
FolderRepositoryError::OperationNotSupported(msg) => {
DomainError::operation_not_supported("Folder", msg)
}
FolderRepositoryError::DomainError(e) => e,
}
}
@@ -7,9 +7,7 @@ use tokio::{fs, io};
use crate::{
application::ports::share_ports::ShareStoragePort,
common::{config::AppConfig, errors::DomainError},
domain::{
entities::share::{Share, ShareItemType},
},
domain::entities::share::{Share, ShareItemType},
};
// Structure for storing in the file system
@@ -74,8 +72,8 @@ impl ShareFsRepository {
/// Converts a file system record to a domain entity
fn to_entity(&self, record: &ShareRecord) -> Share {
let item_type = ShareItemType::try_from(record.item_type.as_str())
.unwrap_or(ShareItemType::File);
let item_type =
ShareItemType::try_from(record.item_type.as_str()).unwrap_or(ShareItemType::File);
let permissions = crate::domain::entities::share::SharePermissions::new(
record.permissions_read,
@@ -119,7 +117,9 @@ impl ShareFsRepository {
#[async_trait]
impl ShareStoragePort for ShareFsRepository {
async fn save_share(&self, share: &Share) -> Result<Share, DomainError> {
let mut shares = self.read_shares().await
let mut shares = self
.read_shares()
.await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
// Check if the link already exists
@@ -135,21 +135,22 @@ impl ShareStoragePort for ShareFsRepository {
shares.push(record);
}
self.write_shares(&shares).await
self.write_shares(&shares)
.await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
Ok(share.clone())
}
async fn find_share_by_id(&self, id: &str) -> Result<Share, DomainError> {
let shares = self.read_shares().await
let shares = self
.read_shares()
.await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
let share = shares.iter()
.find(|s| s.id == id)
.ok_or_else(|| {
DomainError::not_found("Share", format!("Share with ID {} not found", id))
});
let share = shares.iter().find(|s| s.id == id).ok_or_else(|| {
DomainError::not_found("Share", format!("Share with ID {} not found", id))
});
match share {
Ok(record) => Ok(self.to_entity(record)),
@@ -158,14 +159,14 @@ impl ShareStoragePort for ShareFsRepository {
}
async fn find_share_by_token(&self, token: &str) -> Result<Share, DomainError> {
let shares = self.read_shares().await
let shares = self
.read_shares()
.await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
let share = shares.iter()
.find(|s| s.token == token)
.ok_or_else(|| {
DomainError::not_found("Share", format!("Share with token {} not found", token))
});
let share = shares.iter().find(|s| s.token == token).ok_or_else(|| {
DomainError::not_found("Share", format!("Share with token {} not found", token))
});
match share {
Ok(record) => Ok(self.to_entity(record)),
@@ -173,12 +174,19 @@ impl ShareStoragePort for ShareFsRepository {
}
}
async fn find_shares_by_item(&self, item_id: &str, item_type: &ShareItemType) -> Result<Vec<Share>, DomainError> {
let shares = self.read_shares().await
async fn find_shares_by_item(
&self,
item_id: &str,
item_type: &ShareItemType,
) -> Result<Vec<Share>, DomainError> {
let shares = self
.read_shares()
.await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
let type_str = item_type.to_string();
let result: Vec<Share> = shares.iter()
let result: Vec<Share> = shares
.iter()
.filter(|s| s.item_id == item_id && s.item_type == type_str)
.map(|record| self.to_entity(record))
.collect();
@@ -187,27 +195,37 @@ impl ShareStoragePort for ShareFsRepository {
}
async fn update_share(&self, share: &Share) -> Result<Share, DomainError> {
let mut shares = self.read_shares().await
let mut shares = self
.read_shares()
.await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
// Find the index of the link to update
let index = shares.iter().position(|s| s.id == share.id())
let index = shares
.iter()
.position(|s| s.id == share.id())
.ok_or_else(|| {
DomainError::not_found("Share", format!("Share with ID {} not found for update", share.id()))
DomainError::not_found(
"Share",
format!("Share with ID {} not found for update", share.id()),
)
})?;
// Update the record
shares[index] = self.to_record(share);
// Save changes
self.write_shares(&shares).await
self.write_shares(&shares)
.await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
Ok(share.clone())
}
async fn delete_share(&self, id: &str) -> Result<(), DomainError> {
let mut shares = self.read_shares().await
let mut shares = self
.read_shares()
.await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
// Find the index of the link to delete
@@ -216,22 +234,34 @@ impl ShareStoragePort for ShareFsRepository {
// If no link was deleted, it means it didn't exist
if shares.len() == initial_len {
return Err(DomainError::not_found("Share", format!("Share with ID {} not found for deletion", id)));
return Err(DomainError::not_found(
"Share",
format!("Share with ID {} not found for deletion", id),
));
}
// Save changes
self.write_shares(&shares).await
self.write_shares(&shares)
.await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
Ok(())
}
async fn find_shares_by_user(&self, user_id: &str, offset: usize, limit: usize) -> Result<(Vec<Share>, usize), DomainError> {
let shares = self.read_shares().await
async fn find_shares_by_user(
&self,
user_id: &str,
offset: usize,
limit: usize,
) -> Result<(Vec<Share>, usize), DomainError> {
let shares = self
.read_shares()
.await
.map_err(|e| DomainError::internal_error("Share", e.to_string()))?;
// Filter the user's links
let user_shares: Vec<ShareRecord> = shares.into_iter()
let user_shares: Vec<ShareRecord> = shares
.into_iter()
.filter(|s| s.created_by == user_id)
.collect();
@@ -239,7 +269,8 @@ impl ShareStoragePort for ShareFsRepository {
let total = user_shares.len();
// Apply pagination
let paginated: Vec<Share> = user_shares.iter()
let paginated: Vec<Share> = user_shares
.iter()
.skip(offset)
.take(limit)
.map(|record| self.to_entity(record))
@@ -1,16 +1,16 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use async_trait::async_trait;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs;
use uuid::Uuid;
use tracing::{debug, error, instrument};
use uuid::Uuid;
use crate::common::errors::{Result, DomainError, ErrorKind};
use crate::application::ports::outbound::IdMappingPort;
use crate::common::errors::{DomainError, ErrorKind, Result};
use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
use crate::domain::repositories::trash_repository::TrashRepository;
use crate::application::ports::outbound::IdMappingPort;
/// Structure for storing trash items in JSON format
#[derive(Debug, Serialize, Deserialize)]
@@ -38,162 +38,200 @@ impl TrashFsRepository {
) -> Self {
let trash_dir = storage_root.as_ref().join(".trash");
let trash_index_path = trash_dir.join("trash_index.json");
Self {
trash_dir,
trash_index_path,
}
}
/// Ensures the trash directory exists
async fn ensure_trash_dir(&self) -> Result<()> {
debug!("Checking if trash directory exists: {}", self.trash_dir.display());
debug!(
"Checking if trash directory exists: {}",
self.trash_dir.display()
);
if !self.trash_dir.exists() {
debug!("Trash directory does not exist, creating it: {}", self.trash_dir.display());
fs::create_dir_all(&self.trash_dir).await
.map_err(|e| {
error!("Failed to create trash directory {}: {}", self.trash_dir.display(), e);
DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to create trash directory {}: {}", self.trash_dir.display(), e)
)
})?;
debug!(
"Trash directory does not exist, creating it: {}",
self.trash_dir.display()
);
fs::create_dir_all(&self.trash_dir).await.map_err(|e| {
error!(
"Failed to create trash directory {}: {}",
self.trash_dir.display(),
e
);
DomainError::new(
ErrorKind::InternalError,
"Trash",
format!(
"Failed to create trash directory {}: {}",
self.trash_dir.display(),
e
),
)
})?;
debug!("Trash directory created successfully");
} else {
debug!("Trash directory already exists");
}
// Ensure the files directory exists
let files_dir = self.trash_dir.join("files");
debug!("Checking if trash files directory exists: {}", files_dir.display());
debug!(
"Checking if trash files directory exists: {}",
files_dir.display()
);
if !files_dir.exists() {
debug!("Trash files directory does not exist, creating it: {}", files_dir.display());
fs::create_dir_all(&files_dir).await
.map_err(|e| {
error!("Failed to create trash files directory {}: {}", files_dir.display(), e);
DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to create trash files directory {}: {}", files_dir.display(), e)
)
})?;
debug!(
"Trash files directory does not exist, creating it: {}",
files_dir.display()
);
fs::create_dir_all(&files_dir).await.map_err(|e| {
error!(
"Failed to create trash files directory {}: {}",
files_dir.display(),
e
);
DomainError::new(
ErrorKind::InternalError,
"Trash",
format!(
"Failed to create trash files directory {}: {}",
files_dir.display(),
e
),
)
})?;
debug!("Trash files directory created successfully");
} else {
debug!("Trash files directory already exists");
}
// Also ensure the folders directory exists
let folders_dir = self.trash_dir.join("folders");
debug!("Checking if trash folders directory exists: {}", folders_dir.display());
debug!(
"Checking if trash folders directory exists: {}",
folders_dir.display()
);
if !folders_dir.exists() {
debug!("Trash folders directory does not exist, creating it: {}", folders_dir.display());
fs::create_dir_all(&folders_dir).await
.map_err(|e| {
error!("Failed to create trash folders directory {}: {}", folders_dir.display(), e);
DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to create trash folders directory {}: {}", folders_dir.display(), e)
)
})?;
debug!(
"Trash folders directory does not exist, creating it: {}",
folders_dir.display()
);
fs::create_dir_all(&folders_dir).await.map_err(|e| {
error!(
"Failed to create trash folders directory {}: {}",
folders_dir.display(),
e
);
DomainError::new(
ErrorKind::InternalError,
"Trash",
format!(
"Failed to create trash folders directory {}: {}",
folders_dir.display(),
e
),
)
})?;
debug!("Trash folders directory created successfully");
} else {
debug!("Trash folders directory already exists");
}
Ok(())
}
/// Gets all entries from the trash index
async fn get_trash_entries(&self) -> Result<Vec<TrashedItemEntry>> {
self.ensure_trash_dir().await?;
if !self.trash_index_path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&self.trash_index_path).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to read trash index: {}", e)
))?;
let content = fs::read_to_string(&self.trash_index_path)
.await
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to read trash index: {}", e),
)
})?;
if content.trim().is_empty() {
return Ok(Vec::new());
}
let entries: Vec<TrashedItemEntry> = serde_json::from_str(&content)
.map_err(|e| DomainError::new(
let entries: Vec<TrashedItemEntry> = serde_json::from_str(&content).map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to parse trash index: {}", e)
))?;
format!("Failed to parse trash index: {}", e),
)
})?;
Ok(entries)
}
/// Saves all entries to the trash index
async fn save_trash_entries(&self, entries: Vec<TrashedItemEntry>) -> Result<()> {
self.ensure_trash_dir().await?;
let json = serde_json::to_string_pretty(&entries)
.map_err(|e| DomainError::new(
let json = serde_json::to_string_pretty(&entries).map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to serialize trash index: {}", e)
))?;
fs::write(&self.trash_index_path, json).await
.map_err(|e| DomainError::new(
format!("Failed to serialize trash index: {}", e),
)
})?;
fs::write(&self.trash_index_path, json).await.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to write trash index: {}", e)
))?;
format!("Failed to write trash index: {}", e),
)
})?;
Ok(())
}
/// Converts a JSON entry to a TrashedItem entity
fn entry_to_trashed_item(&self, entry: TrashedItemEntry) -> Result<TrashedItem> {
let item_type = match entry.item_type.as_str() {
"file" => TrashedItemType::File,
"folder" => TrashedItemType::Folder,
_ => return Err(DomainError::new(
ErrorKind::InvalidInput,
"Trash",
format!("Invalid trashed item type: {}", entry.item_type)
)),
_ => {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"Trash",
format!("Invalid trashed item type: {}", entry.item_type),
));
}
};
let original_id = Uuid::parse_str(&entry.original_id)
.map_err(|e| DomainError::validation_error(
format!("Invalid original ID format: {}", e)
))?;
let original_id = Uuid::parse_str(&entry.original_id).map_err(|e| {
DomainError::validation_error(format!("Invalid original ID format: {}", e))
})?;
let id = Uuid::parse_str(&entry.id)
.map_err(|e| DomainError::validation_error(
format!("Invalid ID format: {}", e)
))?;
.map_err(|e| DomainError::validation_error(format!("Invalid ID format: {}", e)))?;
let user_id = Uuid::parse_str(&entry.user_id)
.map_err(|e| DomainError::validation_error(
format!("Invalid user ID format: {}", e)
))?;
.map_err(|e| DomainError::validation_error(format!("Invalid user ID format: {}", e)))?;
let trashed_at = chrono::DateTime::parse_from_rfc3339(&entry.trashed_at)
.map_err(|e| DomainError::validation_error(
format!("Invalid trashed_at date: {}", e)
))?
.map_err(|e| DomainError::validation_error(format!("Invalid trashed_at date: {}", e)))?
.with_timezone(&Utc);
let deletion_date = chrono::DateTime::parse_from_rfc3339(&entry.deletion_date)
.map_err(|e| DomainError::validation_error(
format!("Invalid deletion_date: {}", e)
))?
.map_err(|e| DomainError::validation_error(format!("Invalid deletion_date: {}", e)))?
.with_timezone(&Utc);
Ok(TrashedItem::from_raw(
id,
original_id,
@@ -205,7 +243,7 @@ impl TrashFsRepository {
deletion_date,
))
}
/// Converts a TrashedItem entity to a JSON entry
fn trashed_item_to_entry(&self, item: &TrashedItem) -> TrashedItemEntry {
TrashedItemEntry {
@@ -228,55 +266,72 @@ impl TrashFsRepository {
impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))]
async fn add_to_trash(&self, item: &TrashedItem) -> Result<()> {
debug!("Adding item to trash: id={}, user={}", item.id(), item.user_id());
debug!(
"Adding item to trash: id={}, user={}",
item.id(),
item.user_id()
);
// Ensure the trash directory exists for this user
let user_trash_dir = self.trash_dir.join("files").join(item.user_id().to_string());
let user_trash_dir = self
.trash_dir
.join("files")
.join(item.user_id().to_string());
debug!("User trash directory path: {}", user_trash_dir.display());
// Create the user-specific trash directory
debug!("Creating user trash directory: {}", user_trash_dir.display());
debug!(
"Creating user trash directory: {}",
user_trash_dir.display()
);
match fs::create_dir_all(&user_trash_dir).await {
Ok(_) => debug!("User trash directory created successfully"),
Err(e) => {
error!("Failed to create user trash directory {}: {}", user_trash_dir.display(), e);
error!(
"Failed to create user trash directory {}: {}",
user_trash_dir.display(),
e
);
return Err(DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to create user trash directory: {}", e)
format!("Failed to create user trash directory: {}", e),
));
}
}
// Log the current trash entries before adding the new one
let mut entries = self.get_trash_entries().await?;
debug!("Current trash entries count: {}", entries.len());
// Create the entry for the trash index
let entry = self.trashed_item_to_entry(item);
debug!("Created trash entry: id={}, original_id={}, name={}",
entry.id, entry.original_id, entry.name);
debug!(
"Created trash entry: id={}, original_id={}, name={}",
entry.id, entry.original_id, entry.name
);
// Add the entry to the index and save
entries.push(entry);
debug!("Saving updated trash index with {} entries", entries.len());
self.save_trash_entries(entries).await?;
debug!("Trash index updated successfully");
Ok(())
}
#[instrument(skip(self))]
async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> {
debug!("Getting trash items for user: {}", user_id);
let entries = self.get_trash_entries().await?;
let user_id_str = user_id.to_string();
let user_entries = entries.into_iter()
let user_entries = entries
.into_iter()
.filter(|entry| entry.user_id == user_id_str)
.collect::<Vec<_>>();
let mut items = Vec::new();
for entry in user_entries {
match self.entry_to_trashed_item(entry) {
@@ -284,27 +339,28 @@ impl TrashRepository for TrashFsRepository {
Err(e) => error!("Error converting trash entry to item: {}", e),
}
}
Ok(items)
}
#[instrument(skip(self))]
async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result<Option<TrashedItem>> {
debug!("Looking for item in trash: id={}, user={}", id, user_id);
let entries = self.get_trash_entries().await?;
let id_str = id.to_string();
let user_id_str = user_id.to_string();
let item_entry = entries.into_iter()
let item_entry = entries
.into_iter()
.find(|entry| entry.id == id_str && entry.user_id == user_id_str);
match item_entry {
Some(entry) => {
let item = self.entry_to_trashed_item(entry)?;
Ok(Some(item))
},
}
None => Ok(None),
}
}
@@ -312,16 +368,16 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))]
async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
debug!("Restoring item from trash: id={}, user={}", id, user_id);
let mut entries = self.get_trash_entries().await?;
let id_str = id.to_string();
let user_id_str = user_id.to_string();
let index = entries.iter().position(|entry|
entry.id == id_str && entry.user_id == user_id_str
);
let index = entries
.iter()
.position(|entry| entry.id == id_str && entry.user_id == user_id_str);
if let Some(index) = index {
entries.remove(index);
self.save_trash_entries(entries).await?;
@@ -333,8 +389,11 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))]
async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
debug!("Permanently deleting item from trash: id={}, user={}", id, user_id);
debug!(
"Permanently deleting item from trash: id={}, user={}",
id, user_id
);
// Simply remove the entry from the index
// Physical files will be deleted through the corresponding repository
self.restore_from_trash(id, user_id).await
@@ -343,25 +402,25 @@ impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))]
async fn clear_trash(&self, user_id: &Uuid) -> Result<()> {
debug!("Clearing trash for user: {}", user_id);
let mut entries = self.get_trash_entries().await?;
let user_id_str = user_id.to_string();
entries.retain(|entry| entry.user_id != user_id_str);
self.save_trash_entries(entries).await?;
Ok(())
}
#[instrument(skip(self))]
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> {
debug!("Looking for expired trash items");
let entries = self.get_trash_entries().await?;
let now = Utc::now();
let mut expired_items = Vec::new();
for entry in entries {
match chrono::DateTime::parse_from_rfc3339(&entry.deletion_date) {
Ok(date) => {
@@ -372,11 +431,11 @@ impl TrashRepository for TrashFsRepository {
Err(e) => error!("Error converting expired trash entry: {}", e),
}
}
},
}
Err(e) => error!("Invalid date format in trash entry: {}", e),
}
}
Ok(expired_items)
}
}
}