2026-02-12 09:41:25 +01:00
|
|
|
//! Domain persistence port for the Folder entity.
|
2026-02-08 13:40:23 +01:00
|
|
|
//!
|
2026-02-12 09:41:25 +01:00
|
|
|
//! Defines the contract that any folder storage implementation
|
|
|
|
|
//! must fulfill. This trait lives in the domain because Folder is a core entity
|
|
|
|
|
//! of the system and its persistence contracts belong to the domain layer,
|
|
|
|
|
//! following the principles of Clean/Hexagonal Architecture.
|
2026-02-08 13:40:23 +01:00
|
|
|
//!
|
2026-02-12 09:41:25 +01:00
|
|
|
//! Concrete implementations (filesystem, PostgreSQL, S3, etc.) live in
|
|
|
|
|
//! the infrastructure layer.
|
2026-02-08 13:40:23 +01:00
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::common::errors::DomainError;
|
2025-03-17 21:28:08 +01:00
|
|
|
use crate::domain::entities::folder::Folder;
|
2025-03-19 00:44:27 +01:00
|
|
|
use crate::domain::services::path_service::StoragePath;
|
2026-03-07 18:05:52 +01:00
|
|
|
use uuid::Uuid;
|
2025-03-17 21:28:08 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Domain port for folder persistence.
|
2026-02-08 13:40:23 +01:00
|
|
|
///
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Defines the CRUD and management operations required for
|
|
|
|
|
/// the Folder entity in the storage system.
|
2025-03-17 21:28:08 +01:00
|
|
|
pub trait FolderRepository: Send + Sync + 'static {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Creates a new folder
|
2026-02-14 01:29:34 +01:00
|
|
|
async fn create_folder(
|
|
|
|
|
&self,
|
|
|
|
|
name: String,
|
|
|
|
|
parent_id: Option<String>,
|
|
|
|
|
) -> Result<Folder, DomainError>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Gets a folder by its ID
|
2026-02-08 13:40:23 +01:00
|
|
|
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Gets a folder by its storage path
|
2026-02-08 13:40:23 +01:00
|
|
|
async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Lists folders within a parent folder
|
2026-02-08 13:40:23 +01:00
|
|
|
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-15 23:45:11 +01:00
|
|
|
/// Lists root-level folders owned by a specific user.
|
|
|
|
|
/// For non-root queries (parent_id is Some), ownership is implicit
|
|
|
|
|
/// because the parent already belongs to the user.
|
|
|
|
|
async fn list_folders_by_owner(
|
|
|
|
|
&self,
|
|
|
|
|
parent_id: Option<&str>,
|
2026-03-07 18:05:52 +01:00
|
|
|
owner_id: Uuid,
|
2026-02-15 23:45:11 +01:00
|
|
|
) -> Result<Vec<Folder>, DomainError>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// 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,
|
2026-02-08 13:40:23 +01:00
|
|
|
parent_id: Option<&str>,
|
|
|
|
|
offset: usize,
|
2025-03-19 00:44:27 +01:00
|
|
|
limit: usize,
|
2026-02-14 01:29:34 +01:00
|
|
|
include_total: bool,
|
2026-02-08 13:40:23 +01:00
|
|
|
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-16 00:22:42 +01:00
|
|
|
/// Lists folders with pagination, scoped to a specific owner.
|
|
|
|
|
/// Combines the owner filtering of `list_folders_by_owner` with
|
|
|
|
|
/// the pagination of `list_folders_paginated`.
|
|
|
|
|
async fn list_folders_by_owner_paginated(
|
|
|
|
|
&self,
|
|
|
|
|
parent_id: Option<&str>,
|
2026-03-07 18:05:52 +01:00
|
|
|
owner_id: Uuid,
|
2026-02-16 00:22:42 +01:00
|
|
|
offset: usize,
|
|
|
|
|
limit: usize,
|
|
|
|
|
include_total: bool,
|
|
|
|
|
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Renames a folder
|
2026-02-08 13:40:23 +01:00
|
|
|
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Moves a folder to another parent
|
2026-02-14 01:29:34 +01:00
|
|
|
async fn move_folder(
|
|
|
|
|
&self,
|
|
|
|
|
id: &str,
|
|
|
|
|
new_parent_id: Option<&str>,
|
|
|
|
|
) -> Result<Folder, DomainError>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Deletes a folder
|
2026-02-08 13:40:23 +01:00
|
|
|
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Checks if a folder exists at the given path
|
2026-02-08 13:40:23 +01:00
|
|
|
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Gets the path of a folder
|
2026-02-08 13:40:23 +01:00
|
|
|
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
|
|
|
|
|
|
|
|
|
// ── Trash operations ──
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Moves a folder to the trash
|
2026-02-08 13:40:23 +01:00
|
|
|
async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Restores a folder from the trash to its original location
|
2026-02-14 01:29:34 +01:00
|
|
|
async fn restore_from_trash(
|
|
|
|
|
&self,
|
|
|
|
|
folder_id: &str,
|
|
|
|
|
original_path: &str,
|
|
|
|
|
) -> Result<(), DomainError>;
|
2026-02-08 13:40:23 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Permanently deletes a folder (used by the trash)
|
2026-02-08 13:40:23 +01:00
|
|
|
async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>;
|
2026-02-16 16:18:39 +01:00
|
|
|
|
2026-06-18 13:29:41 +02:00
|
|
|
/// Creates a root-level home folder for a user inside their personal drive.
|
|
|
|
|
/// Called during user registration / first login to maintain the wrapper-
|
|
|
|
|
/// folder convention through the D0 dual-write window (the wrapper itself
|
|
|
|
|
/// retires in a follow-up migration; for now it stays as a real folder
|
|
|
|
|
/// row stamped with `drive_id`).
|
|
|
|
|
async fn create_home_folder(
|
|
|
|
|
&self,
|
|
|
|
|
user_id: Uuid,
|
|
|
|
|
drive_id: Uuid,
|
|
|
|
|
name: String,
|
|
|
|
|
) -> Result<Folder, DomainError>;
|
2026-02-23 00:17:40 +01:00
|
|
|
|
2026-02-24 12:18:38 +01:00
|
|
|
/// Lists every folder in a subtree rooted at `folder_id` (inclusive).
|
|
|
|
|
///
|
|
|
|
|
/// Uses ltree `<@` for a single GiST-indexed scan. The result is
|
|
|
|
|
/// ordered by `path` so callers can iterate in directory order.
|
|
|
|
|
///
|
|
|
|
|
/// Default: falls back to `list_folders` (one level only).
|
2026-02-25 10:28:34 +01:00
|
|
|
async fn list_subtree_folders(&self, folder_id: &str) -> Result<Vec<Folder>, DomainError> {
|
2026-02-24 12:18:38 +01:00
|
|
|
let _ = folder_id;
|
|
|
|
|
Ok(Vec::new())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 00:17:40 +01:00
|
|
|
/// Lists all descendant folders in a subtree (ltree-based).
|
|
|
|
|
///
|
|
|
|
|
/// Returns all folders whose lpath is a descendant of the given folder's
|
|
|
|
|
/// lpath. Used for recursive search — O(1) SQL via GiST index instead
|
|
|
|
|
/// of O(N) recursive traversal.
|
|
|
|
|
///
|
|
|
|
|
/// The default implementation returns an empty vec (stubs / mocks).
|
|
|
|
|
async fn list_descendant_folders(
|
|
|
|
|
&self,
|
|
|
|
|
folder_id: &str,
|
|
|
|
|
name_contains: Option<&str>,
|
2026-03-07 18:05:52 +01:00
|
|
|
user_id: Uuid,
|
2026-02-23 00:17:40 +01:00
|
|
|
) -> Result<Vec<Folder>, DomainError> {
|
|
|
|
|
let _ = (folder_id, name_contains, user_id);
|
|
|
|
|
Ok(Vec::new())
|
|
|
|
|
}
|
2026-02-23 23:36:38 +01:00
|
|
|
|
2026-02-24 17:15:36 +01:00
|
|
|
/// Search folders with SQL-level filtering by name, user, and scope.
|
|
|
|
|
///
|
|
|
|
|
/// - **Non-recursive** (`recursive = false`): searches direct children of
|
|
|
|
|
/// `parent_id` (or root folders when `None`).
|
|
|
|
|
/// - **Recursive with `parent_id`**: delegates to `list_descendant_folders`
|
|
|
|
|
/// (ltree GiST-indexed scan).
|
|
|
|
|
/// - **Recursive without `parent_id`**: searches ALL folders owned by
|
|
|
|
|
/// `user_id` with optional name filter in SQL.
|
|
|
|
|
///
|
|
|
|
|
/// The default implementation falls back to `list_folders` + in-memory
|
|
|
|
|
/// filter so that stubs and mocks compile without changes.
|
|
|
|
|
async fn search_folders(
|
|
|
|
|
&self,
|
|
|
|
|
parent_id: Option<&str>,
|
|
|
|
|
name_contains: Option<&str>,
|
2026-03-07 18:05:52 +01:00
|
|
|
user_id: Uuid,
|
2026-02-24 17:15:36 +01:00
|
|
|
recursive: bool,
|
|
|
|
|
) -> Result<Vec<Folder>, DomainError> {
|
|
|
|
|
// Recursive with folder_id → use optimised ltree scan
|
2026-02-25 10:28:34 +01:00
|
|
|
if recursive && let Some(fid) = parent_id {
|
|
|
|
|
return self
|
|
|
|
|
.list_descendant_folders(fid, name_contains, user_id)
|
|
|
|
|
.await;
|
2026-02-24 17:15:36 +01:00
|
|
|
}
|
|
|
|
|
// Fallback: load + filter in memory (stubs / mocks)
|
|
|
|
|
let all = self.list_folders(parent_id).await?;
|
|
|
|
|
match name_contains {
|
|
|
|
|
Some(q) if !q.is_empty() => {
|
|
|
|
|
let q = q.to_lowercase();
|
2026-02-25 10:28:34 +01:00
|
|
|
Ok(all
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter(|f| f.name().to_lowercase().contains(&q))
|
|
|
|
|
.collect())
|
2026-02-24 17:15:36 +01:00
|
|
|
}
|
|
|
|
|
_ => Ok(all),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 23:36:38 +01:00
|
|
|
/// Return up to `limit` folders whose name contains `query` (case-insensitive).
|
|
|
|
|
///
|
|
|
|
|
/// Results are ordered by relevance (exact > starts-with > contains) for
|
|
|
|
|
/// autocomplete suggestions.
|
|
|
|
|
///
|
|
|
|
|
/// The default implementation falls back to `list_folders` + in-memory
|
|
|
|
|
/// filter so that stubs and mocks compile without changes.
|
|
|
|
|
async fn suggest_folders_by_name(
|
|
|
|
|
&self,
|
|
|
|
|
parent_id: Option<&str>,
|
|
|
|
|
query: &str,
|
|
|
|
|
limit: usize,
|
|
|
|
|
) -> Result<Vec<Folder>, DomainError> {
|
|
|
|
|
let all = self.list_folders(parent_id).await?;
|
|
|
|
|
let q = query.to_lowercase();
|
|
|
|
|
let mut matched: Vec<Folder> = all
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter(|f| f.name().to_lowercase().contains(&q))
|
|
|
|
|
.collect();
|
|
|
|
|
matched.truncate(limit);
|
|
|
|
|
Ok(matched)
|
|
|
|
|
}
|
2026-05-05 22:41:55 +02:00
|
|
|
|
|
|
|
|
/// `true` if `candidate_folder_id` is `root_folder_id` itself or any
|
|
|
|
|
/// (transitive) descendant. Default impl fails closed so stubs deny
|
|
|
|
|
/// access by default.
|
|
|
|
|
async fn is_folder_in_subtree(
|
|
|
|
|
&self,
|
|
|
|
|
candidate_folder_id: &str,
|
|
|
|
|
root_folder_id: &str,
|
|
|
|
|
) -> Result<bool, DomainError> {
|
|
|
|
|
let _ = (candidate_folder_id, root_folder_id);
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `true` if `file_id`'s parent folder lies within the subtree rooted
|
|
|
|
|
/// at `root_folder_id`.
|
|
|
|
|
async fn is_file_in_subtree(
|
|
|
|
|
&self,
|
|
|
|
|
file_id: &str,
|
|
|
|
|
root_folder_id: &str,
|
|
|
|
|
) -> Result<bool, DomainError> {
|
|
|
|
|
let _ = (file_id, root_folder_id);
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
2026-02-08 13:40:23 +01:00
|
|
|
}
|