2026-02-14 01:29:34 +01:00
|
|
|
use std::sync::Arc;
|
2025-03-24 16:47:42 +01:00
|
|
|
use tracing::{debug, error, info, instrument};
|
2026-02-14 01:29:34 +01:00
|
|
|
use uuid::Uuid;
|
2025-03-24 16:47:42 +01:00
|
|
|
|
|
|
|
|
use crate::application::dtos::trash_dto::TrashedItemDto;
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
|
|
|
|
|
use crate::application::ports::trash_ports::TrashUseCase;
|
|
|
|
|
use crate::common::errors::{DomainError, ErrorKind, Result};
|
2025-03-24 16:47:42 +01:00
|
|
|
use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
|
2026-03-04 23:55:08 +01:00
|
|
|
use crate::domain::repositories::folder_repository::FolderRepository;
|
2025-03-24 16:47:42 +01:00
|
|
|
use crate::domain::repositories::trash_repository::TrashRepository;
|
2026-03-03 15:36:42 +00:00
|
|
|
use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository;
|
|
|
|
|
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
|
|
|
|
|
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
|
|
|
|
use crate::infrastructure::repositories::pg::trash_db_repository::TrashDbRepository;
|
2025-03-24 16:47:42 +01:00
|
|
|
|
2025-03-26 19:08:07 +01:00
|
|
|
/**
|
|
|
|
|
* Application service for trash operations.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-03-26 19:08:07 +01:00
|
|
|
* The TrashService implements the trash management functionality in the application layer,
|
|
|
|
|
* handling movement of files and folders to trash, restoration from trash, and permanent
|
|
|
|
|
* deletion. It orchestrates interactions between the domain entities and infrastructure
|
|
|
|
|
* repositories while enforcing business rules like retention policies.
|
2026-02-14 01:29:34 +01:00
|
|
|
*
|
2025-03-26 19:08:07 +01:00
|
|
|
* This service follows the Clean Architecture pattern by:
|
2026-02-08 13:40:23 +01:00
|
|
|
* - Depending on application ports rather than domain/infrastructure traits
|
2025-03-26 19:08:07 +01:00
|
|
|
* - Orchestrating domain operations without containing domain logic
|
|
|
|
|
* - Exposing its functionality through the TrashUseCase port
|
|
|
|
|
*/
|
2025-03-24 16:47:42 +01:00
|
|
|
pub struct TrashService {
|
2025-03-26 19:08:07 +01:00
|
|
|
/// Repository for trash-specific operations like listing and retrieving trashed items
|
2026-03-03 15:36:42 +00:00
|
|
|
trash_repository: Arc<TrashDbRepository>,
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
/// Port for file read operations (get file metadata)
|
2026-03-03 15:36:42 +00:00
|
|
|
file_read_port: Arc<FileBlobReadRepository>,
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
/// Port for file write operations (trash, restore, delete)
|
2026-03-03 15:36:42 +00:00
|
|
|
file_write_port: Arc<FileBlobWriteRepository>,
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
/// Port for folder operations (get folder, trash, restore, delete)
|
2026-03-03 15:36:42 +00:00
|
|
|
folder_storage_port: Arc<FolderDbRepository>,
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 19:08:07 +01:00
|
|
|
/// Number of days items should be kept in trash before automatic cleanup
|
2025-03-24 16:47:42 +01:00
|
|
|
retention_days: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TrashService {
|
|
|
|
|
pub fn new(
|
2026-03-03 15:36:42 +00:00
|
|
|
trash_repository: Arc<TrashDbRepository>,
|
|
|
|
|
file_read_port: Arc<FileBlobReadRepository>,
|
|
|
|
|
file_write_port: Arc<FileBlobWriteRepository>,
|
|
|
|
|
folder_storage_port: Arc<FolderDbRepository>,
|
2025-03-24 16:47:42 +01:00
|
|
|
retention_days: u32,
|
|
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
trash_repository,
|
2026-02-08 13:40:23 +01:00
|
|
|
file_read_port,
|
|
|
|
|
file_write_port,
|
|
|
|
|
folder_storage_port,
|
2025-03-24 16:47:42 +01:00
|
|
|
retention_days,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
/// Converts a TrashedItem entity to a DTO
|
2025-03-24 16:47:42 +01:00
|
|
|
fn to_dto(&self, item: TrashedItem) -> TrashedItemDto {
|
2026-02-08 13:40:23 +01:00
|
|
|
// Calculate days_until_deletion before moving item fields
|
2025-03-24 16:47:42 +01:00
|
|
|
let days_until_deletion = item.days_until_deletion();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-24 16:47:42 +01:00
|
|
|
TrashedItemDto {
|
2026-02-08 13:40:23 +01:00
|
|
|
id: item.id().to_string(),
|
|
|
|
|
original_id: item.original_id().to_string(),
|
|
|
|
|
item_type: match item.item_type() {
|
2025-03-24 16:47:42 +01:00
|
|
|
TrashedItemType::File => "file".to_string(),
|
|
|
|
|
TrashedItemType::Folder => "folder".to_string(),
|
|
|
|
|
},
|
2026-02-08 13:40:23 +01:00
|
|
|
name: item.name().to_string(),
|
|
|
|
|
original_path: item.original_path().to_string(),
|
|
|
|
|
trashed_at: item.trashed_at(),
|
2025-03-24 16:47:42 +01:00
|
|
|
days_until_deletion,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 04:02:38 +01:00
|
|
|
/// Validates that the given user owns the trashed item.
|
|
|
|
|
/// Returns an error if the item does not exist or belongs to a different user.
|
2025-03-24 16:47:42 +01:00
|
|
|
#[instrument(skip(self))]
|
2026-02-12 12:29:12 +01:00
|
|
|
async fn _validate_user_ownership(&self, item_id: &str, user_id: &str) -> Result<()> {
|
2026-02-07 04:02:38 +01:00
|
|
|
let item_uuid = Uuid::parse_str(item_id)
|
|
|
|
|
.map_err(|e| DomainError::validation_error(format!("Invalid item ID: {}", e)))?;
|
|
|
|
|
let user_uuid = Uuid::parse_str(user_id)
|
|
|
|
|
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
match self
|
|
|
|
|
.trash_repository
|
|
|
|
|
.get_trash_item(&item_uuid, &user_uuid)
|
|
|
|
|
.await?
|
|
|
|
|
{
|
2026-02-07 04:02:38 +01:00
|
|
|
Some(item) => {
|
2026-02-08 13:40:23 +01:00
|
|
|
if item.user_id() != user_uuid {
|
2026-02-07 04:02:38 +01:00
|
|
|
error!(
|
|
|
|
|
"User {} attempted to access trash item {} owned by {}",
|
2026-02-14 01:29:34 +01:00
|
|
|
user_id,
|
|
|
|
|
item_id,
|
|
|
|
|
item.user_id()
|
2026-02-07 04:02:38 +01:00
|
|
|
);
|
|
|
|
|
return Err(DomainError::access_denied(
|
|
|
|
|
"TrashItem",
|
|
|
|
|
"You do not have permission to access this trash item",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
// Item not found for this user — treat as authorization error
|
|
|
|
|
// to avoid leaking existence information
|
|
|
|
|
Err(DomainError::not_found(
|
|
|
|
|
"TrashItem",
|
|
|
|
|
format!("{} (user: {})", item_id, user_id),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-24 16:47:42 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TrashUseCase for TrashService {
|
|
|
|
|
#[instrument(skip(self))]
|
|
|
|
|
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> {
|
2025-03-30 14:17:09 +00:00
|
|
|
debug!("Getting trash items for user: {}", user_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-24 16:47:42 +01:00
|
|
|
let user_uuid = Uuid::parse_str(user_id)
|
2025-04-13 01:04:04 +02:00
|
|
|
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-24 16:47:42 +01:00
|
|
|
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
let dtos = items.into_iter().map(|item| self.to_dto(item)).collect();
|
|
|
|
|
|
2025-03-24 16:47:42 +01:00
|
|
|
Ok(dtos)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[instrument(skip(self))]
|
|
|
|
|
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()> {
|
2026-02-14 01:29:34 +01:00
|
|
|
info!(
|
|
|
|
|
"Moving to trash: type={}, id={}, user={}",
|
|
|
|
|
item_type, item_id, user_id
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
debug!("User UUID validation: {}", user_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-05 13:15:34 +01:00
|
|
|
// Note: We now verify file/folder ownership BEFORE moving to trash.
|
|
|
|
|
// This prevents users from trashing items they do not own (IDOR).
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
// Parse UUIDs with detailed error handling
|
2025-03-30 14:17:09 +00:00
|
|
|
debug!("Validating item UUID: {}", item_id);
|
2025-03-26 18:33:22 +01:00
|
|
|
let item_uuid = match Uuid::parse_str(item_id) {
|
|
|
|
|
Ok(uuid) => {
|
2025-03-30 14:17:09 +00:00
|
|
|
debug!("Valid item UUID: {}", uuid);
|
2025-03-26 18:33:22 +01:00
|
|
|
uuid
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
2025-03-30 14:17:09 +00:00
|
|
|
error!("Invalid item UUID: {} - Error: {}", item_id, e);
|
2026-02-14 01:29:34 +01:00
|
|
|
return Err(DomainError::validation_error(format!(
|
|
|
|
|
"Invalid item ID: {}",
|
|
|
|
|
e
|
|
|
|
|
)));
|
2025-03-26 18:33:22 +01:00
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
debug!("Validating user UUID: {}", user_id);
|
2025-03-26 18:33:22 +01:00
|
|
|
let user_uuid = match Uuid::parse_str(user_id) {
|
|
|
|
|
Ok(uuid) => {
|
2025-03-30 14:17:09 +00:00
|
|
|
debug!("Valid user UUID: {}", uuid);
|
2025-03-26 18:33:22 +01:00
|
|
|
uuid
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
2025-03-30 14:17:09 +00:00
|
|
|
error!("Invalid user UUID: {} - Error: {}", user_id, e);
|
2026-02-14 01:29:34 +01:00
|
|
|
return Err(DomainError::validation_error(format!(
|
|
|
|
|
"Invalid user ID: {}",
|
|
|
|
|
e
|
|
|
|
|
)));
|
2025-03-26 18:33:22 +01:00
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-24 16:47:42 +01:00
|
|
|
match item_type {
|
|
|
|
|
"file" => {
|
2025-03-30 14:17:09 +00:00
|
|
|
info!("Processing file to move to trash: {}", item_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-05 13:15:34 +01:00
|
|
|
// Get the file — ownership-verified at SQL level.
|
|
|
|
|
// Returns NotFound if the file does not exist OR belongs to
|
|
|
|
|
// another user, preventing cross-user trash operations.
|
|
|
|
|
debug!("Getting file data (owner-scoped): {}", item_id);
|
|
|
|
|
let file = match self.file_read_port.get_file_for_owner(item_id, user_id).await {
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(file) => {
|
2025-03-30 14:17:09 +00:00
|
|
|
debug!("File found: {} ({})", file.name(), item_id);
|
2025-03-26 18:33:22 +01:00
|
|
|
file
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
2025-03-30 14:17:09 +00:00
|
|
|
error!("Error getting file: {} - {}", item_id, e);
|
2025-03-26 18:33:22 +01:00
|
|
|
return Err(DomainError::new(
|
|
|
|
|
ErrorKind::NotFound,
|
|
|
|
|
"File",
|
2026-02-14 01:29:34 +01:00
|
|
|
format!("Error retrieving file {}: {}", item_id, e),
|
2025-03-26 18:33:22 +01:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-24 16:47:42 +01:00
|
|
|
let original_path = file.storage_path().to_string();
|
2025-03-30 14:17:09 +00:00
|
|
|
debug!("Original file path: {}", original_path);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
// Create the trash item
|
|
|
|
|
debug!("Creating TrashedItem object for the file");
|
2025-03-24 16:47:42 +01:00
|
|
|
let trashed_item = TrashedItem::new(
|
|
|
|
|
item_uuid,
|
|
|
|
|
user_uuid,
|
|
|
|
|
TrashedItemType::File,
|
|
|
|
|
file.name().to_string(),
|
|
|
|
|
original_path,
|
|
|
|
|
self.retention_days,
|
|
|
|
|
);
|
2026-02-14 01:29:34 +01:00
|
|
|
debug!(
|
|
|
|
|
"TrashedItem created successfully: {} -> {}",
|
|
|
|
|
file.name(),
|
|
|
|
|
trashed_item.id()
|
|
|
|
|
);
|
|
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
// First add to trash index to register the item
|
|
|
|
|
info!("Adding file {} to trash index", item_id);
|
2025-03-26 18:33:22 +01:00
|
|
|
match self.trash_repository.add_to_trash(&trashed_item).await {
|
|
|
|
|
Ok(_) => {
|
2025-03-30 14:17:09 +00:00
|
|
|
debug!("File added to trash index successfully");
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
2025-03-30 14:17:09 +00:00
|
|
|
error!("Error adding file to trash index: {}", e);
|
2026-02-14 01:29:34 +01:00
|
|
|
return Err(DomainError::internal_error(
|
|
|
|
|
"TrashRepository",
|
|
|
|
|
format!("Failed to add file to trash: {}", e),
|
|
|
|
|
));
|
2025-03-26 18:33:22 +01:00
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
// Then physically move the file to trash
|
|
|
|
|
info!("Physically moving file to trash: {}", item_id);
|
2026-02-08 13:40:23 +01:00
|
|
|
match self.file_write_port.move_to_trash(item_id).await {
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(_) => {
|
2025-03-30 14:17:09 +00:00
|
|
|
debug!("File physically moved to trash successfully: {}", item_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
2025-03-30 14:17:09 +00:00
|
|
|
error!("Error physically moving file to trash: {} - {}", item_id, e);
|
2025-03-26 18:33:22 +01:00
|
|
|
return Err(DomainError::new(
|
|
|
|
|
ErrorKind::InternalError,
|
|
|
|
|
"File",
|
2026-02-14 01:29:34 +01:00
|
|
|
format!("Error moving file {} to trash: {}", item_id, e),
|
2025-03-26 18:33:22 +01:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
info!("File completely moved to trash: {}", item_id);
|
2025-03-24 16:47:42 +01:00
|
|
|
Ok(())
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-24 16:47:42 +01:00
|
|
|
"folder" => {
|
2026-03-05 13:15:34 +01:00
|
|
|
// Get the folder and verify ownership.
|
|
|
|
|
// Returns NotFound if the folder does not exist or belongs
|
|
|
|
|
// to another user — prevents cross-user trash operations.
|
2026-02-14 01:29:34 +01:00
|
|
|
let folder = self
|
|
|
|
|
.folder_storage_port
|
|
|
|
|
.get_folder(item_id)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
DomainError::new(
|
|
|
|
|
ErrorKind::NotFound,
|
|
|
|
|
"Folder",
|
|
|
|
|
format!("Error retrieving folder {}: {}", item_id, e),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
2026-03-05 13:15:34 +01:00
|
|
|
// Ownership check — return NotFound (not Forbidden) to
|
|
|
|
|
// prevent leaking whether the folder exists.
|
2026-03-05 21:23:00 +01:00
|
|
|
if folder.owner_id().is_none_or(|o| o != user_id) {
|
2026-03-05 13:15:34 +01:00
|
|
|
return Err(DomainError::not_found(
|
|
|
|
|
"Folder",
|
|
|
|
|
format!("Folder not found: {}", item_id),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 16:47:42 +01:00
|
|
|
let original_path = folder.storage_path().to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
// Create the trash item
|
2025-03-24 16:47:42 +01:00
|
|
|
let trashed_item = TrashedItem::new(
|
|
|
|
|
item_uuid,
|
|
|
|
|
user_uuid,
|
|
|
|
|
TrashedItemType::Folder,
|
|
|
|
|
folder.name().to_string(),
|
|
|
|
|
original_path,
|
|
|
|
|
self.retention_days,
|
|
|
|
|
);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
// First add to trash index to register the item
|
2025-03-26 18:33:22 +01:00
|
|
|
debug!("Adding folder {} to trash repository", item_id);
|
|
|
|
|
match self.trash_repository.add_to_trash(&trashed_item).await {
|
|
|
|
|
Ok(_) => debug!("Successfully added folder to trash repository"),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("Failed to add folder to trash repository: {}", e);
|
2026-02-14 01:29:34 +01:00
|
|
|
return Err(DomainError::internal_error(
|
|
|
|
|
"TrashRepository",
|
|
|
|
|
format!("Failed to add folder to trash: {}", e),
|
|
|
|
|
));
|
2025-03-26 18:33:22 +01:00
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
// Then physically move the folder to trash
|
2026-02-14 01:29:34 +01:00
|
|
|
self.folder_storage_port
|
|
|
|
|
.move_to_trash(item_id)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
DomainError::new(
|
|
|
|
|
ErrorKind::InternalError,
|
|
|
|
|
"Folder",
|
|
|
|
|
format!("Error moving folder {} to trash: {}", item_id, e),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
debug!("Folder moved to trash: {}", item_id);
|
2025-03-24 16:47:42 +01:00
|
|
|
Ok(())
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
_ => Err(DomainError::validation_error(format!(
|
|
|
|
|
"Invalid item type: {}",
|
|
|
|
|
item_type
|
|
|
|
|
))),
|
2025-03-24 16:47:42 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[instrument(skip(self))]
|
|
|
|
|
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> {
|
2025-03-30 14:17:09 +00:00
|
|
|
info!("Restoring item {} for user {}", trash_id, user_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
let trash_uuid = match Uuid::parse_str(trash_id) {
|
|
|
|
|
Ok(id) => {
|
|
|
|
|
info!("Trash UUID parsed successfully: {}", id);
|
|
|
|
|
id
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
|
|
|
|
error!("Invalid trash ID format: {} - {}", trash_id, e);
|
2026-02-14 01:29:34 +01:00
|
|
|
return Err(DomainError::validation_error(format!(
|
|
|
|
|
"Invalid trash ID: {}",
|
|
|
|
|
e
|
|
|
|
|
)));
|
2025-03-26 18:33:22 +01:00
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
let user_uuid = match Uuid::parse_str(user_id) {
|
|
|
|
|
Ok(id) => {
|
|
|
|
|
info!("User UUID parsed successfully: {}", id);
|
|
|
|
|
id
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
|
|
|
|
error!("Invalid user ID format: {} - {}", user_id, e);
|
2026-02-14 01:29:34 +01:00
|
|
|
return Err(DomainError::validation_error(format!(
|
|
|
|
|
"Invalid user ID: {}",
|
|
|
|
|
e
|
|
|
|
|
)));
|
2025-03-26 18:33:22 +01:00
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Get the trash item
|
2026-02-12 11:26:23 +01:00
|
|
|
info!("Retrieving trash item from repository: ID={}", trash_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
let item_result = self
|
|
|
|
|
.trash_repository
|
|
|
|
|
.get_trash_item(&trash_uuid, &user_uuid)
|
|
|
|
|
.await;
|
2026-02-12 11:26:23 +01:00
|
|
|
|
|
|
|
|
match item_result {
|
|
|
|
|
Ok(Some(item)) => {
|
2026-02-14 01:29:34 +01:00
|
|
|
info!(
|
|
|
|
|
"Found item in trash: ID={}, Type={:?}, OriginalID={}",
|
|
|
|
|
trash_id,
|
|
|
|
|
item.item_type(),
|
|
|
|
|
item.original_id()
|
|
|
|
|
);
|
2026-02-12 11:26:23 +01:00
|
|
|
|
|
|
|
|
// Restore based on type
|
|
|
|
|
match item.item_type() {
|
2025-03-26 18:33:22 +01:00
|
|
|
TrashedItemType::File => {
|
2025-03-30 14:17:09 +00:00
|
|
|
// Restore the file to its original location
|
2026-02-08 13:40:23 +01:00
|
|
|
let file_id = item.original_id().to_string();
|
|
|
|
|
let original_path = item.original_path().to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
info!(
|
|
|
|
|
"Restoring file from trash: ID={}, OriginalPath={}",
|
|
|
|
|
file_id, original_path
|
|
|
|
|
);
|
|
|
|
|
match self
|
|
|
|
|
.file_write_port
|
|
|
|
|
.restore_from_trash(&file_id, &original_path)
|
|
|
|
|
.await
|
|
|
|
|
{
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(_) => {
|
|
|
|
|
info!("Successfully restored file from trash: {}", file_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
|
|
|
|
// Check if the error is because the file is not found
|
|
|
|
|
if format!("{}", e).contains("not found") {
|
2026-02-14 01:29:34 +01:00
|
|
|
info!(
|
|
|
|
|
"File not found in trash, may already have been restored: {}",
|
|
|
|
|
file_id
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
// We continue so we can clean up the trash entry
|
|
|
|
|
} else {
|
|
|
|
|
// Return error for other kinds of errors
|
|
|
|
|
error!("Error restoring file from trash: {} - {}", file_id, e);
|
|
|
|
|
return Err(DomainError::new(
|
|
|
|
|
ErrorKind::InternalError,
|
|
|
|
|
"File",
|
2026-02-14 01:29:34 +01:00
|
|
|
format!(
|
|
|
|
|
"Error restoring file {} from trash: {}",
|
|
|
|
|
file_id, e
|
|
|
|
|
),
|
2025-03-26 18:33:22 +01:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
TrashedItemType::Folder => {
|
2025-03-30 14:17:09 +00:00
|
|
|
// Restore the folder to its original location
|
2026-02-08 13:40:23 +01:00
|
|
|
let folder_id = item.original_id().to_string();
|
|
|
|
|
let original_path = item.original_path().to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
info!(
|
|
|
|
|
"Restoring folder from trash: ID={}, OriginalPath={}",
|
|
|
|
|
folder_id, original_path
|
|
|
|
|
);
|
|
|
|
|
match self
|
|
|
|
|
.folder_storage_port
|
|
|
|
|
.restore_from_trash(&folder_id, &original_path)
|
|
|
|
|
.await
|
|
|
|
|
{
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(_) => {
|
|
|
|
|
info!("Successfully restored folder from trash: {}", folder_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
|
|
|
|
// Check if the error is because the folder is not found
|
|
|
|
|
if format!("{}", e).contains("not found") {
|
2026-02-14 01:29:34 +01:00
|
|
|
info!(
|
|
|
|
|
"Folder not found in trash, may already have been restored: {}",
|
|
|
|
|
folder_id
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
// We continue so we can clean up the trash entry
|
|
|
|
|
} else {
|
|
|
|
|
// Return error for other kinds of errors
|
2026-02-14 01:29:34 +01:00
|
|
|
error!(
|
|
|
|
|
"Error restoring folder from trash: {} - {}",
|
|
|
|
|
folder_id, e
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
return Err(DomainError::new(
|
|
|
|
|
ErrorKind::InternalError,
|
|
|
|
|
"Folder",
|
2026-02-14 01:29:34 +01:00
|
|
|
format!(
|
|
|
|
|
"Error restoring folder {} from trash: {}",
|
|
|
|
|
folder_id, e
|
|
|
|
|
),
|
2025-03-26 18:33:22 +01:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
// Always remove the item from the trash index to maintain consistency
|
2026-02-14 01:29:34 +01:00
|
|
|
info!(
|
|
|
|
|
"Removing item from trash index after restoration: {}",
|
|
|
|
|
trash_id
|
|
|
|
|
);
|
|
|
|
|
match self
|
|
|
|
|
.trash_repository
|
|
|
|
|
.restore_from_trash(&trash_uuid, &user_uuid)
|
|
|
|
|
.await
|
|
|
|
|
{
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(_) => {
|
|
|
|
|
info!("Successfully removed entry from trash index: {}", trash_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
2026-02-14 01:29:34 +01:00
|
|
|
error!(
|
|
|
|
|
"Error removing entry from trash index: {} - {}",
|
|
|
|
|
trash_id, e
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
return Err(DomainError::new(
|
|
|
|
|
ErrorKind::InternalError,
|
|
|
|
|
"Trash",
|
2026-02-14 01:29:34 +01:00
|
|
|
format!("Error removing trash entry after restoration: {}", e),
|
2025-03-26 18:33:22 +01:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
info!("Item successfully restored from trash: {}", trash_id);
|
|
|
|
|
Ok(())
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(None) => {
|
|
|
|
|
// If the item isn't found in trash, we can just return success
|
2026-02-14 01:29:34 +01:00
|
|
|
info!(
|
|
|
|
|
"Item not found in trash index, considering as already restored: {}",
|
|
|
|
|
trash_id
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(())
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
|
|
|
|
// Something went wrong with the repository
|
2026-02-14 01:29:34 +01:00
|
|
|
error!(
|
|
|
|
|
"Error retrieving item from trash repository: {} - {}",
|
|
|
|
|
trash_id, e
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e)
|
2025-03-24 16:47:42 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[instrument(skip(self))]
|
|
|
|
|
async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()> {
|
2026-02-14 01:29:34 +01:00
|
|
|
info!(
|
|
|
|
|
"Permanently deleting item {} for user {}",
|
|
|
|
|
trash_id, user_id
|
|
|
|
|
);
|
|
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
let trash_uuid = match Uuid::parse_str(trash_id) {
|
|
|
|
|
Ok(id) => {
|
|
|
|
|
info!("Trash UUID parsed successfully: {}", id);
|
|
|
|
|
id
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
|
|
|
|
error!("Invalid trash ID format: {} - {}", trash_id, e);
|
2026-02-14 01:29:34 +01:00
|
|
|
return Err(DomainError::validation_error(format!(
|
|
|
|
|
"Invalid trash ID: {}",
|
|
|
|
|
e
|
|
|
|
|
)));
|
2025-03-26 18:33:22 +01:00
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
let user_uuid = match Uuid::parse_str(user_id) {
|
|
|
|
|
Ok(id) => {
|
|
|
|
|
info!("User UUID parsed successfully: {}", id);
|
|
|
|
|
id
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
|
|
|
|
error!("Invalid user ID format: {} - {}", user_id, e);
|
2026-02-14 01:29:34 +01:00
|
|
|
return Err(DomainError::validation_error(format!(
|
|
|
|
|
"Invalid user ID: {}",
|
|
|
|
|
e
|
|
|
|
|
)));
|
2025-03-26 18:33:22 +01:00
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Get the trash item
|
2025-03-26 18:33:22 +01:00
|
|
|
info!("Retrieving trash item from repository: ID={}", trash_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
let item_result = self
|
|
|
|
|
.trash_repository
|
|
|
|
|
.get_trash_item(&trash_uuid, &user_uuid)
|
|
|
|
|
.await;
|
|
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
match item_result {
|
|
|
|
|
Ok(Some(item)) => {
|
2026-02-14 01:29:34 +01:00
|
|
|
info!(
|
|
|
|
|
"Found item in trash: ID={}, Type={:?}, OriginalID={}",
|
|
|
|
|
trash_id,
|
|
|
|
|
item.item_type(),
|
|
|
|
|
item.original_id()
|
|
|
|
|
);
|
|
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
// Permanently delete based on type
|
2026-02-08 13:40:23 +01:00
|
|
|
match item.item_type() {
|
2025-03-26 18:33:22 +01:00
|
|
|
TrashedItemType::File => {
|
2026-02-12 09:41:25 +01:00
|
|
|
// Permanently delete the file
|
2026-02-08 13:40:23 +01:00
|
|
|
let file_id = item.original_id().to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
info!("Permanently deleting file: {}", file_id);
|
2026-02-08 13:40:23 +01:00
|
|
|
match self.file_write_port.delete_file_permanently(&file_id).await {
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(_) => {
|
|
|
|
|
info!("Successfully deleted file permanently: {}", file_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
|
|
|
|
// Check if the file is not found - in that case, we can continue
|
|
|
|
|
// because we still want to remove the item from the trash index
|
|
|
|
|
if format!("{}", e).contains("not found") {
|
2026-02-14 01:29:34 +01:00
|
|
|
info!(
|
|
|
|
|
"File not found, may already have been deleted: {}",
|
|
|
|
|
file_id
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
} else {
|
|
|
|
|
// Return error for other types of errors
|
|
|
|
|
error!("Error permanently deleting file: {} - {}", file_id, e);
|
|
|
|
|
return Err(DomainError::new(
|
|
|
|
|
ErrorKind::InternalError,
|
|
|
|
|
"File",
|
2026-02-14 01:29:34 +01:00
|
|
|
format!(
|
|
|
|
|
"Error deleting file {} permanently: {}",
|
|
|
|
|
file_id, e
|
|
|
|
|
),
|
2025-03-26 18:33:22 +01:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
TrashedItemType::Folder => {
|
2026-02-12 09:41:25 +01:00
|
|
|
// Permanently delete the folder
|
2026-02-08 13:40:23 +01:00
|
|
|
let folder_id = item.original_id().to_string();
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
info!("Permanently deleting folder: {}", folder_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
match self
|
|
|
|
|
.folder_storage_port
|
|
|
|
|
.delete_folder_permanently(&folder_id)
|
|
|
|
|
.await
|
|
|
|
|
{
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(_) => {
|
|
|
|
|
info!("Successfully deleted folder permanently: {}", folder_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
|
|
|
|
// Check if the folder is not found - in that case, we can continue
|
|
|
|
|
if format!("{}", e).contains("not found") {
|
2026-02-14 01:29:34 +01:00
|
|
|
info!(
|
|
|
|
|
"Folder not found, may already have been deleted: {}",
|
|
|
|
|
folder_id
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
} else {
|
|
|
|
|
// Return error for other types of errors
|
2026-02-14 01:29:34 +01:00
|
|
|
error!(
|
|
|
|
|
"Error permanently deleting folder: {} - {}",
|
|
|
|
|
folder_id, e
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
return Err(DomainError::new(
|
|
|
|
|
ErrorKind::InternalError,
|
|
|
|
|
"Folder",
|
2026-02-14 01:29:34 +01:00
|
|
|
format!(
|
|
|
|
|
"Error deleting folder {} permanently: {}",
|
|
|
|
|
folder_id, e
|
|
|
|
|
),
|
2025-03-26 18:33:22 +01:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-30 14:17:09 +00:00
|
|
|
// Always remove the item from trash index to maintain consistency
|
2025-03-26 18:33:22 +01:00
|
|
|
info!("Removing entry from trash index: {}", trash_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
match self
|
|
|
|
|
.trash_repository
|
|
|
|
|
.delete_permanently(&trash_uuid, &user_uuid)
|
|
|
|
|
.await
|
|
|
|
|
{
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(_) => {
|
|
|
|
|
info!("Successfully removed entry from trash index: {}", trash_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
2026-02-14 01:29:34 +01:00
|
|
|
error!(
|
|
|
|
|
"Error removing entry from trash index: {} - {}",
|
|
|
|
|
trash_id, e
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
return Err(DomainError::new(
|
|
|
|
|
ErrorKind::InternalError,
|
|
|
|
|
"Trash",
|
2026-02-14 01:29:34 +01:00
|
|
|
format!("Error removing trash entry: {}", e),
|
2025-03-26 18:33:22 +01:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
info!("Item permanently deleted from trash: {}", trash_id);
|
|
|
|
|
Ok(())
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(None) => {
|
|
|
|
|
// If the item isn't found in trash, we can just return success
|
2026-02-14 01:29:34 +01:00
|
|
|
info!(
|
|
|
|
|
"Item not found in trash, considering as already deleted: {}",
|
|
|
|
|
trash_id
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
Ok(())
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e) => {
|
|
|
|
|
// Something went wrong with the repository
|
2026-02-14 01:29:34 +01:00
|
|
|
error!(
|
|
|
|
|
"Error retrieving item from trash repository: {} - {}",
|
|
|
|
|
trash_id, e
|
|
|
|
|
);
|
2025-03-26 18:33:22 +01:00
|
|
|
Err(e)
|
2025-03-24 16:47:42 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[instrument(skip(self))]
|
|
|
|
|
async fn empty_trash(&self, user_id: &str) -> Result<()> {
|
2025-03-30 14:17:09 +00:00
|
|
|
info!("Emptying trash for user {}", user_id);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-24 16:47:42 +01:00
|
|
|
let user_uuid = Uuid::parse_str(user_id)
|
2025-04-13 01:04:04 +02:00
|
|
|
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-25 10:02:25 +01:00
|
|
|
// clear_trash() already performs bulk SQL DELETEs in 2 queries:
|
|
|
|
|
// 1. DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE
|
|
|
|
|
// 2. DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE
|
|
|
|
|
//
|
|
|
|
|
// Folder deletion cascades (FK ON DELETE CASCADE) to child folders and
|
|
|
|
|
// their files. The PG trigger `trg_files_decrement_blob_ref` automatically
|
|
|
|
|
// decrements blob ref_counts for every deleted file row — no Rust-side
|
|
|
|
|
// remove_reference() call is needed.
|
|
|
|
|
//
|
|
|
|
|
// Finally it clears the trash_items index for the user.
|
2025-03-24 16:47:42 +01:00
|
|
|
self.trash_repository.clear_trash(&user_uuid).await?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-25 10:02:25 +01:00
|
|
|
info!("Trash emptied for user {}", user_id);
|
2025-03-24 16:47:42 +01:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|