Files
Oxicloud/src/application/services/folder_service.rs
T

548 lines
18 KiB
Rust
Raw Normal View History

2026-02-14 01:29:34 +01:00
use crate::application::dtos::folder_dto::{
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
};
2025-03-19 00:44:27 +01:00
use crate::application::ports::inbound::FolderUseCase;
use crate::application::ports::outbound::FolderStoragePort;
use crate::application::transactions::storage_transaction::StorageTransaction;
2025-03-26 18:33:22 +01:00
use crate::common::errors::{DomainError, ErrorKind};
2026-02-14 01:29:34 +01:00
use crate::domain::services::path_service::StoragePath;
use async_trait::async_trait;
use std::sync::Arc;
2025-03-17 21:28:08 +01:00
/// Implementation of the use case for folder operations
2025-03-17 21:28:08 +01:00
pub struct FolderService {
2025-03-19 00:44:27 +01:00
folder_storage: Arc<dyn FolderStoragePort>,
2025-03-17 21:28:08 +01:00
}
impl FolderService {
/// Creates a new folder service
2025-03-19 00:44:27 +01:00
pub fn new(folder_storage: Arc<dyn FolderStoragePort>) -> Self {
Self { folder_storage }
2025-03-17 21:28:08 +01:00
}
2026-02-14 01:29:34 +01:00
2025-03-20 09:22:31 +01:00
/// Creates a stub implementation for testing and middleware
pub fn new_stub() -> impl FolderUseCase {
struct FolderServiceStub;
2026-02-14 01:29:34 +01:00
2025-03-20 09:22:31 +01:00
#[async_trait]
impl FolderUseCase for FolderServiceStub {
async fn create_folder(&self, _dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty())
}
2026-02-14 01:29:34 +01:00
2025-03-20 09:22:31 +01:00
async fn get_folder(&self, _id: &str) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty())
}
2026-02-14 01:29:34 +01:00
2025-03-20 09:22:31 +01:00
async fn get_folder_by_path(&self, _path: &str) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty())
}
2026-02-14 01:29:34 +01:00
async fn list_folders(
&self,
_parent_id: Option<&str>,
) -> Result<Vec<FolderDto>, DomainError> {
2025-03-20 09:22:31 +01:00
Ok(vec![])
}
2026-02-14 01:29:34 +01:00
async fn list_folders_for_owner(
&self,
_parent_id: Option<&str>,
_owner_id: &str,
) -> Result<Vec<FolderDto>, DomainError> {
Ok(vec![])
}
2025-03-20 09:22:31 +01:00
async fn list_folders_paginated(
2026-02-14 01:29:34 +01:00
&self,
2025-03-20 09:22:31 +01:00
_parent_id: Option<&str>,
2026-02-14 01:29:34 +01:00
_pagination: &crate::application::dtos::pagination::PaginationRequestDto,
) -> Result<
crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>,
DomainError,
> {
Ok(
crate::application::dtos::pagination::PaginatedResponseDto::new(
vec![],
0,
10,
0,
),
)
2025-03-20 09:22:31 +01:00
}
2026-02-14 01:29:34 +01:00
async fn list_folders_for_owner_paginated(
&self,
_parent_id: Option<&str>,
_owner_id: &str,
_pagination: &crate::application::dtos::pagination::PaginationRequestDto,
) -> Result<
crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>,
DomainError,
> {
Ok(
crate::application::dtos::pagination::PaginatedResponseDto::new(
vec![],
0,
10,
0,
),
)
}
2026-02-14 01:29:34 +01:00
async fn rename_folder(
&self,
_id: &str,
_dto: RenameFolderDto,
_caller_id: &str,
2026-02-14 01:29:34 +01:00
) -> Result<FolderDto, DomainError> {
2025-03-20 09:22:31 +01:00
Ok(FolderDto::empty())
}
2026-02-14 01:29:34 +01:00
async fn move_folder(
&self,
_id: &str,
_dto: MoveFolderDto,
_caller_id: &str,
2026-02-14 01:29:34 +01:00
) -> Result<FolderDto, DomainError> {
2025-03-20 09:22:31 +01:00
Ok(FolderDto::empty())
}
2026-02-14 01:29:34 +01:00
async fn delete_folder(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> {
2025-03-20 09:22:31 +01:00
Ok(())
}
async fn create_home_folder(
&self,
_user_id: &str,
_name: String,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty())
}
2025-03-20 09:22:31 +01:00
}
2026-02-14 01:29:34 +01:00
2025-03-20 09:22:31 +01:00
FolderServiceStub
}
2025-03-19 00:44:27 +01:00
}
#[async_trait]
impl FolderUseCase for FolderService {
/// Creates a new folder
2025-03-19 00:44:27 +01:00
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
// Input validation
2025-03-19 00:44:27 +01:00
if dto.name.is_empty() {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"Folder",
2026-02-14 01:29:34 +01:00
"Folder name cannot be empty",
2025-03-19 00:44:27 +01:00
));
}
2026-02-14 01:29:34 +01:00
// If a parent_id is provided, verify it exists
2025-03-19 00:44:27 +01:00
if let Some(parent_id) = &dto.parent_id {
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
if !parent_exists {
return Err(DomainError::not_found("Folder", parent_id));
}
}
2026-02-14 01:29:34 +01:00
// Create the folder
2026-02-14 01:29:34 +01:00
let folder = self
.folder_storage
.create_folder(dto.name, dto.parent_id)
2025-03-19 00:44:27 +01:00
.await
2026-02-14 01:29:34 +01:00
.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to create folder: {}", e),
)
})?;
// Convert to DTO
2025-03-17 21:28:08 +01:00
Ok(FolderDto::from(folder))
}
2026-02-14 01:29:34 +01:00
/// Creates a root-level home folder for a user during registration.
async fn create_home_folder(
&self,
user_id: &str,
name: String,
) -> Result<FolderDto, DomainError> {
let folder = self
.folder_storage
.create_home_folder(user_id, name)
.await
.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to create home folder: {}", e),
)
})?;
Ok(FolderDto::from(folder))
}
/// Gets a folder by its ID
2025-03-19 00:44:27 +01:00
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
2026-02-14 01:29:34 +01:00
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to get folder with ID: {}: {}", id, e),
)
})?;
2025-03-17 21:28:08 +01:00
Ok(FolderDto::from(folder))
}
2026-02-14 01:29:34 +01:00
/// Gets a folder by its path
2025-03-19 00:44:27 +01:00
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError> {
// Convert the string path to StoragePath
2025-03-19 00:44:27 +01:00
let storage_path = StoragePath::from_string(path);
2026-02-14 01:29:34 +01:00
let folder = self
.folder_storage
.get_folder_by_path(&storage_path)
2025-03-19 00:44:27 +01:00
.await
2026-02-14 01:29:34 +01:00
.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to get folder at path: {}: {}", path, e),
)
})?;
2025-03-17 21:28:08 +01:00
Ok(FolderDto::from(folder))
}
2026-02-14 01:29:34 +01:00
/// Lists folders within a parent folder
2025-03-19 00:44:27 +01:00
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> {
2026-02-14 01:29:34 +01:00
let folders = self
.folder_storage
.list_folders(parent_id)
2025-03-19 00:44:27 +01:00
.await
2026-02-14 01:29:34 +01:00
.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to list folders in parent: {:?}: {}", parent_id, e),
)
})?;
// Convert to DTOs
2025-03-17 21:28:08 +01:00
Ok(folders.into_iter().map(FolderDto::from).collect())
}
2026-02-14 01:29:34 +01:00
/// Lists folders scoped to a specific owner.
async fn list_folders_for_owner(
&self,
parent_id: Option<&str>,
owner_id: &str,
) -> Result<Vec<FolderDto>, DomainError> {
let folders = self
.folder_storage
.list_folders_by_owner(parent_id, owner_id)
.await
.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!(
"Failed to list folders for owner '{}' in parent {:?}: {}",
owner_id, parent_id, e
),
)
})?;
Ok(folders.into_iter().map(FolderDto::from).collect())
}
/// Lists folders with pagination
2025-03-19 00:44:27 +01:00
async fn list_folders_paginated(
2026-02-14 01:29:34 +01:00
&self,
2025-03-19 00:44:27 +01:00
parent_id: Option<&str>,
2026-02-14 01:29:34 +01:00
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>
{
2025-03-19 00:44:27 +01:00
let pagination = pagination.validate_and_adjust();
2026-02-14 01:29:34 +01:00
let (folders, total_items) = self
.folder_storage
.list_folders_paginated(parent_id, pagination.offset(), pagination.limit(), true)
2026-02-14 01:29:34 +01:00
.await
.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!(
"Failed to list folders with pagination in parent: {:?}: {}",
parent_id, e
),
)
})?;
2025-03-19 00:44:27 +01:00
let total = total_items.unwrap_or(folders.len());
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
let response = crate::application::dtos::pagination::PaginatedResponseDto::new(
folders.into_iter().map(FolderDto::from).collect(),
pagination.page,
pagination.page_size,
2026-02-14 01:29:34 +01:00
total,
2025-03-19 00:44:27 +01:00
);
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok(response)
}
2026-02-14 01:29:34 +01:00
/// Lists folders with pagination, scoped to a specific owner.
async fn list_folders_for_owner_paginated(
&self,
parent_id: Option<&str>,
owner_id: &str,
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>
{
let pagination = pagination.validate_and_adjust();
let (folders, total_items) = self
.folder_storage
.list_folders_by_owner_paginated(
parent_id,
owner_id,
pagination.offset(),
pagination.limit(),
true,
)
.await
.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!(
"Failed to list folders for owner '{}' with pagination in parent {:?}: {}",
owner_id, parent_id, e
),
)
})?;
let total = total_items.unwrap_or(folders.len());
let response = crate::application::dtos::pagination::PaginatedResponseDto::new(
folders.into_iter().map(FolderDto::from).collect(),
pagination.page,
pagination.page_size,
total,
);
Ok(response)
}
/// Renames a folder after verifying ownership.
2026-02-14 01:29:34 +01:00
async fn rename_folder(
&self,
id: &str,
dto: RenameFolderDto,
caller_id: &str,
2026-02-14 01:29:34 +01:00
) -> Result<FolderDto, DomainError> {
// Input validation
2025-03-19 00:44:27 +01:00
if dto.name.is_empty() {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"Folder",
2026-02-14 01:29:34 +01:00
"New folder name cannot be empty",
2025-03-19 00:44:27 +01:00
));
}
2026-02-14 01:29:34 +01:00
// Verify the folder exists and belongs to the caller
2026-02-14 01:29:34 +01:00
let existing_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to get folder with ID: {} for renaming: {}", id, e),
)
})?;
if existing_folder.owner_id() != Some(caller_id) {
tracing::warn!(
"rename_folder: user '{}' attempted to rename folder '{}' owned by '{:?}'",
caller_id,
id,
existing_folder.owner_id()
);
return Err(DomainError::not_found("Folder", id));
}
// Create transaction for renaming
2025-03-19 00:44:27 +01:00
let mut transaction = StorageTransaction::new("rename_folder");
2026-02-14 01:29:34 +01:00
// Main operation: rename folder
2025-03-19 00:44:27 +01:00
// Clone all values to avoid lifetime issues
let folder_storage = self.folder_storage.clone();
let id_owned = id.to_string();
let name_owned = dto.name.clone();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
// Create future with owned values
let rename_op = async move {
folder_storage.rename_folder(&id_owned, name_owned).await?;
Ok(())
};
let rollback_op = {
let original_name = existing_folder.name().to_string();
let storage = self.folder_storage.clone();
let id_clone = id.to_string();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
async move {
// In case of failure, restore the original name
2026-02-14 01:29:34 +01:00
storage
.rename_folder(&id_clone, original_name)
.await
2025-03-19 00:44:27 +01:00
.map(|_| ())
2026-02-14 01:29:34 +01:00
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"Folder",
format!("Failed to rollback folder rename: {}", e),
)
})
2025-03-19 00:44:27 +01:00
}
};
2026-02-14 01:29:34 +01:00
// Add to the transaction
2025-03-19 00:44:27 +01:00
transaction.add_operation(rename_op, rollback_op);
2026-02-14 01:29:34 +01:00
// Execute transaction
2025-03-19 00:44:27 +01:00
transaction.commit().await?;
2026-02-14 01:29:34 +01:00
// Get the renamed folder
2026-02-14 01:29:34 +01:00
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to get renamed folder with ID: {}: {}", id, e),
)
})?;
2025-03-17 21:28:08 +01:00
Ok(FolderDto::from(folder))
}
2026-02-14 01:29:34 +01:00
/// Moves a folder to a new parent after verifying ownership.
async fn move_folder(
&self,
id: &str,
dto: MoveFolderDto,
caller_id: &str,
) -> Result<FolderDto, DomainError> {
// Verify the source folder exists and belongs to the caller
2026-02-14 01:29:34 +01:00
let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to get folder with ID: {} for moving: {}", id, e),
)
})?;
if source_folder.owner_id() != Some(caller_id) {
tracing::warn!(
"move_folder: user '{}' attempted to move folder '{}' owned by '{:?}'",
caller_id,
id,
source_folder.owner_id()
);
return Err(DomainError::not_found("Folder", id));
}
// If a parent_id is specified, verify it exists
2025-03-19 00:44:27 +01:00
if let Some(parent_id) = &dto.parent_id {
// Verify we are not trying to move the folder into itself or one of its descendants
2025-03-19 00:44:27 +01:00
if parent_id == id {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"Folder",
2026-02-14 01:29:34 +01:00
"Cannot move a folder into itself",
2025-03-19 00:44:27 +01:00
));
}
2026-02-14 01:29:34 +01:00
// Verify the destination exists
2025-03-19 00:44:27 +01:00
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
if !parent_exists {
return Err(DomainError::not_found("Folder", parent_id));
}
2026-02-14 01:29:34 +01:00
// TODO: Ideally we should verify the entire hierarchy to prevent cycles
2025-03-19 00:44:27 +01:00
}
2026-02-14 01:29:34 +01:00
// Create transaction for moving
2025-03-19 00:44:27 +01:00
let mut transaction = StorageTransaction::new("move_folder");
2026-02-14 01:29:34 +01:00
// Main operation: move folder
2025-03-19 00:44:27 +01:00
// Clone all values to avoid lifetime issues
let folder_storage = self.folder_storage.clone();
let id_owned = id.to_string();
// Get parent ID as owned string or None
let parent_id_owned = dto.parent_id.as_ref().map(|p| p.to_string());
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
// Create future with owned values
let move_op = async move {
// Convert Option<String> to Option<&str>
let parent_ref = parent_id_owned.as_deref();
folder_storage.move_folder(&id_owned, parent_ref).await?;
Ok(())
};
let rollback_op = {
let original_parent_id = source_folder.parent_id().map(String::from);
let storage = self.folder_storage.clone();
let id_clone = id.to_string();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
async move {
// In case of failure, restore the original location
2026-02-14 01:29:34 +01:00
storage
.move_folder(&id_clone, original_parent_id.as_deref())
.await
2025-03-19 00:44:27 +01:00
.map(|_| ())
2026-02-14 01:29:34 +01:00
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"Folder",
format!("Failed to rollback folder move: {}", e),
)
})
2025-03-19 00:44:27 +01:00
}
};
2026-02-14 01:29:34 +01:00
// Add to the transaction
2025-03-19 00:44:27 +01:00
transaction.add_operation(move_op, rollback_op);
2026-02-14 01:29:34 +01:00
// Execute transaction
2025-03-19 00:44:27 +01:00
transaction.commit().await?;
2026-02-14 01:29:34 +01:00
// Get the moved folder
2026-02-14 01:29:34 +01:00
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to get moved folder with ID: {}: {}", id, e),
)
})?;
2025-03-17 21:28:08 +01:00
Ok(FolderDto::from(folder))
}
2026-02-14 01:29:34 +01:00
/// Deletes a folder after verifying ownership.
async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError> {
// Verify the folder exists and belongs to the caller
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
2026-02-14 01:29:34 +01:00
DomainError::internal_error(
"FolderStorage",
format!("Failed to get folder with ID: {} for deletion: {}", id, e),
)
})?;
if folder.owner_id() != Some(caller_id) {
tracing::warn!(
"delete_folder: user '{}' attempted to delete folder '{}' owned by '{:?}'",
caller_id,
id,
folder.owner_id()
);
return Err(DomainError::not_found("Folder", id));
}
2026-02-14 01:29:34 +01:00
// Delete the folder
2026-02-14 01:29:34 +01:00
self.folder_storage.delete_folder(id).await.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to delete folder with ID: {}: {}", id, e),
)
})
2025-03-17 21:28:08 +01:00
}
2026-02-14 01:29:34 +01:00
}