refactoring hexagonal and clean architecture
This commit is contained in:
+294
-32
@@ -3,28 +3,79 @@ use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AddressBook {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub owner_id: String,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_public: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
id: Uuid,
|
||||
name: String,
|
||||
owner_id: String,
|
||||
description: Option<String>,
|
||||
color: Option<String>,
|
||||
is_public: bool,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl AddressBook {
|
||||
/// Creates a new AddressBook with generated id and timestamps
|
||||
pub fn new(
|
||||
name: String,
|
||||
owner_id: String,
|
||||
description: Option<String>,
|
||||
color: Option<String>,
|
||||
is_public: bool,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
name,
|
||||
owner_id,
|
||||
description,
|
||||
color,
|
||||
is_public,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstructs from persistence (no validation)
|
||||
pub fn from_raw(
|
||||
id: Uuid,
|
||||
name: String,
|
||||
owner_id: String,
|
||||
description: Option<String>,
|
||||
color: Option<String>,
|
||||
is_public: bool,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
) -> Self {
|
||||
Self { id, name, owner_id, description, color, is_public, created_at, updated_at }
|
||||
}
|
||||
|
||||
// --- Getters ---
|
||||
pub fn id(&self) -> &Uuid { &self.id }
|
||||
pub fn name(&self) -> &str { &self.name }
|
||||
pub fn owner_id(&self) -> &str { &self.owner_id }
|
||||
pub fn description(&self) -> Option<&str> { self.description.as_deref() }
|
||||
pub fn color(&self) -> Option<&str> { self.color.as_deref() }
|
||||
pub fn is_public(&self) -> bool { self.is_public }
|
||||
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
|
||||
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
|
||||
|
||||
// --- Setters for mutable operations ---
|
||||
pub fn set_name(&mut self, name: String) { self.name = name; self.updated_at = Utc::now(); }
|
||||
pub fn set_description(&mut self, description: Option<String>) { self.description = description; self.updated_at = Utc::now(); }
|
||||
pub fn set_color(&mut self, color: Option<String>) { self.color = color; self.updated_at = Utc::now(); }
|
||||
pub fn set_is_public(&mut self, is_public: bool) { self.is_public = is_public; self.updated_at = Utc::now(); }
|
||||
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
|
||||
}
|
||||
|
||||
impl Default for AddressBook {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
name: "Default Address Book".to_string(),
|
||||
owner_id: "default".to_string(),
|
||||
description: None,
|
||||
color: None,
|
||||
is_public: false,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
}
|
||||
Self::new(
|
||||
"Default Address Book".to_string(),
|
||||
"default".to_string(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +106,192 @@ pub struct Address {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Contact {
|
||||
id: Uuid,
|
||||
address_book_id: Uuid,
|
||||
uid: String,
|
||||
full_name: Option<String>,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
nickname: Option<String>,
|
||||
email: Vec<Email>,
|
||||
phone: Vec<Phone>,
|
||||
address: Vec<Address>,
|
||||
organization: Option<String>,
|
||||
title: Option<String>,
|
||||
notes: Option<String>,
|
||||
photo_url: Option<String>,
|
||||
birthday: Option<NaiveDate>,
|
||||
anniversary: Option<NaiveDate>,
|
||||
vcard: String,
|
||||
etag: String,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Contact {
|
||||
/// Creates a new Contact with generated id, uid, etag and timestamps
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
address_book_id: Uuid,
|
||||
full_name: Option<String>,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
nickname: Option<String>,
|
||||
email: Vec<Email>,
|
||||
phone: Vec<Phone>,
|
||||
address: Vec<Address>,
|
||||
organization: Option<String>,
|
||||
title: Option<String>,
|
||||
notes: Option<String>,
|
||||
photo_url: Option<String>,
|
||||
birthday: Option<NaiveDate>,
|
||||
anniversary: Option<NaiveDate>,
|
||||
vcard: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
address_book_id,
|
||||
uid: format!("{}@oxicloud", Uuid::new_v4()),
|
||||
full_name,
|
||||
first_name,
|
||||
last_name,
|
||||
nickname,
|
||||
email,
|
||||
phone,
|
||||
address,
|
||||
organization,
|
||||
title,
|
||||
notes,
|
||||
photo_url,
|
||||
birthday,
|
||||
anniversary,
|
||||
vcard,
|
||||
etag: Uuid::new_v4().to_string(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstructs from persistence (no validation)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn from_raw(
|
||||
id: Uuid,
|
||||
address_book_id: Uuid,
|
||||
uid: String,
|
||||
full_name: Option<String>,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
nickname: Option<String>,
|
||||
email: Vec<Email>,
|
||||
phone: Vec<Phone>,
|
||||
address: Vec<Address>,
|
||||
organization: Option<String>,
|
||||
title: Option<String>,
|
||||
notes: Option<String>,
|
||||
photo_url: Option<String>,
|
||||
birthday: Option<NaiveDate>,
|
||||
anniversary: Option<NaiveDate>,
|
||||
vcard: String,
|
||||
etag: String,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
) -> Self {
|
||||
Self {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Getters ---
|
||||
pub fn id(&self) -> &Uuid { &self.id }
|
||||
pub fn address_book_id(&self) -> &Uuid { &self.address_book_id }
|
||||
pub fn uid(&self) -> &str { &self.uid }
|
||||
pub fn full_name(&self) -> Option<&str> { self.full_name.as_deref() }
|
||||
pub fn first_name(&self) -> Option<&str> { self.first_name.as_deref() }
|
||||
pub fn last_name(&self) -> Option<&str> { self.last_name.as_deref() }
|
||||
pub fn nickname(&self) -> Option<&str> { self.nickname.as_deref() }
|
||||
pub fn email(&self) -> &[Email] { &self.email }
|
||||
pub fn phone(&self) -> &[Phone] { &self.phone }
|
||||
pub fn address(&self) -> &[Address] { &self.address }
|
||||
pub fn organization(&self) -> Option<&str> { self.organization.as_deref() }
|
||||
pub fn title(&self) -> Option<&str> { self.title.as_deref() }
|
||||
pub fn notes(&self) -> Option<&str> { self.notes.as_deref() }
|
||||
pub fn photo_url(&self) -> Option<&str> { self.photo_url.as_deref() }
|
||||
pub fn birthday(&self) -> Option<&NaiveDate> { self.birthday.as_ref() }
|
||||
pub fn anniversary(&self) -> Option<&NaiveDate> { self.anniversary.as_ref() }
|
||||
pub fn vcard(&self) -> &str { &self.vcard }
|
||||
pub fn etag(&self) -> &str { &self.etag }
|
||||
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
|
||||
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
|
||||
|
||||
// --- Owned getters for persistence layer bind() calls ---
|
||||
pub fn full_name_owned(&self) -> Option<String> { self.full_name.clone() }
|
||||
pub fn first_name_owned(&self) -> Option<String> { self.first_name.clone() }
|
||||
pub fn last_name_owned(&self) -> Option<String> { self.last_name.clone() }
|
||||
pub fn nickname_owned(&self) -> Option<String> { self.nickname.clone() }
|
||||
pub fn organization_owned(&self) -> Option<String> { self.organization.clone() }
|
||||
pub fn title_owned(&self) -> Option<String> { self.title.clone() }
|
||||
pub fn notes_owned(&self) -> Option<String> { self.notes.clone() }
|
||||
pub fn photo_url_owned(&self) -> Option<String> { self.photo_url.clone() }
|
||||
|
||||
// --- Setters for mutable operations (contact_service.rs needs these) ---
|
||||
pub fn set_full_name(&mut self, v: Option<String>) { self.full_name = v; }
|
||||
pub fn set_first_name(&mut self, v: Option<String>) { self.first_name = v; }
|
||||
pub fn set_last_name(&mut self, v: Option<String>) { self.last_name = v; }
|
||||
pub fn set_nickname(&mut self, v: Option<String>) { self.nickname = v; }
|
||||
pub fn set_organization(&mut self, v: Option<String>) { self.organization = v; }
|
||||
pub fn set_title(&mut self, v: Option<String>) { self.title = v; }
|
||||
pub fn set_notes(&mut self, v: Option<String>) { self.notes = v; }
|
||||
pub fn set_photo_url(&mut self, v: Option<String>) { self.photo_url = v; }
|
||||
pub fn set_birthday(&mut self, v: Option<NaiveDate>) { self.birthday = v; }
|
||||
pub fn set_anniversary(&mut self, v: Option<NaiveDate>) { self.anniversary = v; }
|
||||
pub fn set_vcard(&mut self, vcard: String) { self.vcard = vcard; }
|
||||
pub fn set_etag(&mut self, etag: String) { self.etag = etag; }
|
||||
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
|
||||
pub fn set_address_book_id(&mut self, id: Uuid) { self.address_book_id = id; }
|
||||
pub fn set_uid(&mut self, uid: String) { self.uid = uid; }
|
||||
|
||||
// --- Collection mutators ---
|
||||
pub fn push_email(&mut self, e: Email) { self.email.push(e); }
|
||||
pub fn push_phone(&mut self, p: Phone) { self.phone.push(p); }
|
||||
pub fn set_email(&mut self, email: Vec<Email>) { self.email = email; }
|
||||
pub fn set_phone(&mut self, phone: Vec<Phone>) { self.phone = phone; }
|
||||
pub fn set_address(&mut self, address: Vec<Address>) { self.address = address; }
|
||||
pub fn email_is_empty(&self) -> bool { self.email.is_empty() }
|
||||
pub fn phone_is_empty(&self) -> bool { self.phone.is_empty() }
|
||||
|
||||
// --- Consuming methods for ownership transfer ---
|
||||
pub fn into_email(self) -> Vec<Email> { self.email }
|
||||
pub fn into_parts(self) -> ContactParts {
|
||||
ContactParts {
|
||||
id: self.id,
|
||||
address_book_id: self.address_book_id,
|
||||
uid: self.uid,
|
||||
full_name: self.full_name,
|
||||
first_name: self.first_name,
|
||||
last_name: self.last_name,
|
||||
nickname: self.nickname,
|
||||
email: self.email,
|
||||
phone: self.phone,
|
||||
address: self.address,
|
||||
organization: self.organization,
|
||||
title: self.title,
|
||||
notes: self.notes,
|
||||
photo_url: self.photo_url,
|
||||
birthday: self.birthday,
|
||||
anniversary: self.anniversary,
|
||||
vcard: self.vcard,
|
||||
etag: self.etag,
|
||||
created_at: self.created_at,
|
||||
updated_at: self.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds all Contact fields by value, for when ownership transfer is needed
|
||||
pub struct ContactParts {
|
||||
pub id: Uuid,
|
||||
pub address_book_id: Uuid,
|
||||
pub uid: String,
|
||||
@@ -79,6 +316,7 @@ pub struct Contact {
|
||||
|
||||
impl Default for Contact {
|
||||
fn default() -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
address_book_id: Uuid::new_v4(),
|
||||
@@ -98,29 +336,53 @@ impl Default for Contact {
|
||||
anniversary: None,
|
||||
vcard: "BEGIN:VCARD\nVERSION:3.0\nEND:VCARD".to_string(),
|
||||
etag: Uuid::new_v4().to_string(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContactGroup {
|
||||
pub id: Uuid,
|
||||
pub address_book_id: Uuid,
|
||||
pub name: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
id: Uuid,
|
||||
address_book_id: Uuid,
|
||||
name: String,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ContactGroup {
|
||||
/// Creates a new ContactGroup with generated id and timestamps
|
||||
pub fn new(address_book_id: Uuid, name: String) -> Self {
|
||||
let now = Utc::now();
|
||||
Self { id: Uuid::new_v4(), address_book_id, name, created_at: now, updated_at: now }
|
||||
}
|
||||
|
||||
/// Reconstructs from persistence
|
||||
pub fn from_raw(
|
||||
id: Uuid,
|
||||
address_book_id: Uuid,
|
||||
name: String,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
) -> Self {
|
||||
Self { id, address_book_id, name, created_at, updated_at }
|
||||
}
|
||||
|
||||
// --- Getters ---
|
||||
pub fn id(&self) -> &Uuid { &self.id }
|
||||
pub fn address_book_id(&self) -> &Uuid { &self.address_book_id }
|
||||
pub fn name(&self) -> &str { &self.name }
|
||||
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
|
||||
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
|
||||
|
||||
// --- Setters ---
|
||||
pub fn set_name(&mut self, name: String) { self.name = name; self.updated_at = Utc::now(); }
|
||||
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
|
||||
}
|
||||
|
||||
impl Default for ContactGroup {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
address_book_id: Uuid::new_v4(),
|
||||
name: "New Group".to_string(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
}
|
||||
ContactGroup::new(Uuid::new_v4(), "New Group".to_string())
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,14 @@ use chrono::{DateTime, Utc, Duration};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Session {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub refresh_token: String,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub ip_address: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub revoked: bool,
|
||||
id: String,
|
||||
user_id: String,
|
||||
refresh_token: String,
|
||||
expires_at: DateTime<Utc>,
|
||||
ip_address: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
created_at: DateTime<Utc>,
|
||||
revoked: bool,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@@ -21,6 +21,13 @@ impl Session {
|
||||
user_agent: Option<String>,
|
||||
expires_in_days: i64,
|
||||
) -> Self {
|
||||
if user_id.is_empty() {
|
||||
panic!("Session user_id cannot be empty");
|
||||
}
|
||||
if refresh_token.is_empty() {
|
||||
panic!("Session refresh_token cannot be empty");
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
@@ -33,6 +40,30 @@ impl Session {
|
||||
revoked: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct a Session from persisted data (e.g. database row).
|
||||
/// Skips ID generation — uses the provided values directly.
|
||||
pub fn from_raw(
|
||||
id: String,
|
||||
user_id: String,
|
||||
refresh_token: String,
|
||||
expires_at: DateTime<Utc>,
|
||||
ip_address: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
created_at: DateTime<Utc>,
|
||||
revoked: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
user_id,
|
||||
refresh_token,
|
||||
expires_at,
|
||||
ip_address,
|
||||
user_agent,
|
||||
created_at,
|
||||
revoked,
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
pub fn id(&self) -> &str {
|
||||
@@ -50,6 +81,14 @@ impl Session {
|
||||
pub fn expires_at(&self) -> DateTime<Utc> {
|
||||
self.expires_at
|
||||
}
|
||||
|
||||
pub fn ip_address(&self) -> Option<&str> {
|
||||
self.ip_address.as_deref()
|
||||
}
|
||||
|
||||
pub fn user_agent(&self) -> Option<&str> {
|
||||
self.user_agent.as_deref()
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> DateTime<Utc> {
|
||||
self.created_at
|
||||
|
||||
+149
-35
@@ -6,23 +6,23 @@ pub use super::entity_errors::ShareError;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Share {
|
||||
pub id: String,
|
||||
pub item_id: String,
|
||||
pub item_type: ShareItemType,
|
||||
pub token: String,
|
||||
pub password_hash: Option<String>,
|
||||
pub expires_at: Option<u64>,
|
||||
pub permissions: SharePermissions,
|
||||
pub created_at: u64,
|
||||
pub created_by: String,
|
||||
pub access_count: u64,
|
||||
id: String,
|
||||
item_id: String,
|
||||
item_type: ShareItemType,
|
||||
token: String,
|
||||
password_hash: Option<String>,
|
||||
expires_at: Option<u64>,
|
||||
permissions: SharePermissions,
|
||||
created_at: u64,
|
||||
created_by: String,
|
||||
access_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SharePermissions {
|
||||
pub read: bool,
|
||||
pub write: bool,
|
||||
pub reshare: bool,
|
||||
read: bool,
|
||||
write: bool,
|
||||
reshare: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -80,6 +80,74 @@ impl Share {
|
||||
})
|
||||
}
|
||||
|
||||
/// Reconstruct a Share from persisted data (e.g. filesystem/database).
|
||||
/// Skips validation and ID generation — uses the provided values directly.
|
||||
pub fn from_raw(
|
||||
id: String,
|
||||
item_id: String,
|
||||
item_type: ShareItemType,
|
||||
token: String,
|
||||
password_hash: Option<String>,
|
||||
expires_at: Option<u64>,
|
||||
permissions: SharePermissions,
|
||||
created_at: u64,
|
||||
created_by: String,
|
||||
access_count: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
item_id,
|
||||
item_type,
|
||||
token,
|
||||
password_hash,
|
||||
expires_at,
|
||||
permissions,
|
||||
created_at,
|
||||
created_by,
|
||||
access_count,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Getters ──
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn item_id(&self) -> &str {
|
||||
&self.item_id
|
||||
}
|
||||
|
||||
pub fn item_type(&self) -> &ShareItemType {
|
||||
&self.item_type
|
||||
}
|
||||
|
||||
pub fn token(&self) -> &str {
|
||||
&self.token
|
||||
}
|
||||
|
||||
pub fn expires_at(&self) -> Option<u64> {
|
||||
self.expires_at
|
||||
}
|
||||
|
||||
pub fn permissions(&self) -> &SharePermissions {
|
||||
&self.permissions
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> u64 {
|
||||
self.created_at
|
||||
}
|
||||
|
||||
pub fn created_by(&self) -> &str {
|
||||
&self.created_by
|
||||
}
|
||||
|
||||
pub fn access_count(&self) -> u64 {
|
||||
self.access_count
|
||||
}
|
||||
|
||||
// ── Builder-style modifiers (immutable) ──
|
||||
|
||||
pub fn with_permissions(mut self, permissions: SharePermissions) -> Self {
|
||||
self.permissions = permissions;
|
||||
self
|
||||
@@ -118,15 +186,17 @@ impl Share {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn verify_password(&self, password: &str) -> bool {
|
||||
match &self.password_hash {
|
||||
Some(hash) => {
|
||||
// In a real implementation, use a proper password hashing function like bcrypt
|
||||
// For simplicity, we're just comparing strings here
|
||||
hash == password
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
/// Returns whether this share requires a password to access.
|
||||
pub fn has_password(&self) -> bool {
|
||||
self.password_hash.is_some()
|
||||
}
|
||||
|
||||
/// Returns a reference to the password hash, if one is set.
|
||||
///
|
||||
/// Password verification should be performed externally via PasswordHasherPort
|
||||
/// to keep cryptographic dependencies out of the domain layer.
|
||||
pub fn password_hash(&self) -> Option<&str> {
|
||||
self.password_hash.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,13 +208,25 @@ impl SharePermissions {
|
||||
reshare,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self) -> bool {
|
||||
self.read
|
||||
}
|
||||
|
||||
pub fn write(&self) -> bool {
|
||||
self.write
|
||||
}
|
||||
|
||||
pub fn reshare(&self) -> bool {
|
||||
self.reshare
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for ShareItemType {
|
||||
fn to_string(&self) -> String {
|
||||
impl std::fmt::Display for ShareItemType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ShareItemType::File => "file".to_string(),
|
||||
ShareItemType::Folder => "folder".to_string(),
|
||||
ShareItemType::File => write!(f, "file"),
|
||||
ShareItemType::Folder => write!(f, "folder"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,15 +259,15 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(share.item_id, "test_file_id");
|
||||
assert_eq!(share.item_type, ShareItemType::File);
|
||||
assert_eq!(share.created_by, "user123");
|
||||
assert_eq!(share.permissions.read, true);
|
||||
assert_eq!(share.permissions.write, false);
|
||||
assert_eq!(share.permissions.reshare, false);
|
||||
assert!(share.password_hash.is_none());
|
||||
assert!(share.expires_at.is_none());
|
||||
assert_eq!(share.access_count, 0);
|
||||
assert_eq!(share.item_id(), "test_file_id");
|
||||
assert_eq!(*share.item_type(), ShareItemType::File);
|
||||
assert_eq!(share.created_by(), "user123");
|
||||
assert_eq!(share.permissions().read(), true);
|
||||
assert_eq!(share.permissions().write(), false);
|
||||
assert_eq!(share.permissions().reshare(), false);
|
||||
assert!(!share.has_password());
|
||||
assert!(share.expires_at().is_none());
|
||||
assert_eq!(share.access_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -233,4 +315,36 @@ mod tests {
|
||||
assert_eq!(ShareItemType::try_from("FILE").unwrap(), ShareItemType::File);
|
||||
assert!(ShareItemType::try_from("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_password_with_hash() {
|
||||
let share = Share::new(
|
||||
"test_file_id".to_string(),
|
||||
ShareItemType::File,
|
||||
"user123".to_string(),
|
||||
None,
|
||||
Some("some_hash_value".to_string()),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(share.has_password());
|
||||
assert_eq!(share.password_hash(), Some("some_hash_value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_password_without_hash() {
|
||||
let share = Share::new(
|
||||
"test_file_id".to_string(),
|
||||
ShareItemType::File,
|
||||
"user123".to_string(),
|
||||
None,
|
||||
None, // No password
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(!share.has_password());
|
||||
assert_eq!(share.password_hash(), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ pub enum TrashedItemType {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrashedItem {
|
||||
pub id: Uuid,
|
||||
pub original_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub item_type: TrashedItemType,
|
||||
pub name: String,
|
||||
pub original_path: String,
|
||||
pub trashed_at: DateTime<Utc>,
|
||||
pub deletion_date: DateTime<Utc>, // Fecha de eliminación permanente automática
|
||||
id: Uuid,
|
||||
original_id: Uuid,
|
||||
user_id: Uuid,
|
||||
item_type: TrashedItemType,
|
||||
name: String,
|
||||
original_path: String,
|
||||
trashed_at: DateTime<Utc>,
|
||||
deletion_date: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl TrashedItem {
|
||||
@@ -41,6 +41,64 @@ impl TrashedItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct a TrashedItem from persisted data (e.g. JSON index).
|
||||
/// Skips ID generation — uses the provided values directly.
|
||||
pub fn from_raw(
|
||||
id: Uuid,
|
||||
original_id: Uuid,
|
||||
user_id: Uuid,
|
||||
item_type: TrashedItemType,
|
||||
name: String,
|
||||
original_path: String,
|
||||
trashed_at: DateTime<Utc>,
|
||||
deletion_date: DateTime<Utc>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
original_id,
|
||||
user_id,
|
||||
item_type,
|
||||
name,
|
||||
original_path,
|
||||
trashed_at,
|
||||
deletion_date,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Getters ──
|
||||
|
||||
pub fn id(&self) -> Uuid {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn original_id(&self) -> Uuid {
|
||||
self.original_id
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> Uuid {
|
||||
self.user_id
|
||||
}
|
||||
|
||||
pub fn item_type(&self) -> &TrashedItemType {
|
||||
&self.item_type
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn original_path(&self) -> &str {
|
||||
&self.original_path
|
||||
}
|
||||
|
||||
pub fn trashed_at(&self) -> DateTime<Utc> {
|
||||
self.trashed_at
|
||||
}
|
||||
|
||||
pub fn deletion_date(&self) -> DateTime<Utc> {
|
||||
self.deletion_date
|
||||
}
|
||||
|
||||
pub fn days_until_deletion(&self) -> i64 {
|
||||
let now = Utc::now();
|
||||
(self.deletion_date - now).num_days().max(0)
|
||||
|
||||
@@ -1,297 +1,143 @@
|
||||
//! Puerto de persistencia del dominio para la entidad File.
|
||||
//!
|
||||
//! Define el contrato que cualquier implementación de almacenamiento de archivos
|
||||
//! debe cumplir. Este trait vive en el dominio porque File es una entidad core
|
||||
//! del sistema y sus contratos de persistencia pertenecen a la capa de dominio,
|
||||
//! siguiendo los principios de Clean/Hexagonal Architecture.
|
||||
//!
|
||||
//! Las implementaciones concretas (filesystem, PostgreSQL, S3, etc.) viven en
|
||||
//! la capa de infraestructura.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::errors::DomainError;
|
||||
use futures::Stream;
|
||||
use bytes::Bytes;
|
||||
|
||||
/**
|
||||
* Comprehensive error types for file repository operations.
|
||||
*
|
||||
* This enum represents all possible error conditions that can occur during file repository
|
||||
* operations, providing detailed context for error handling across the application.
|
||||
*/
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FileRepositoryError {
|
||||
/// Returned when a requested file cannot be found by ID or path
|
||||
#[error("File not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
/// Returned when attempting to create a file at a location where one already exists
|
||||
#[error("File already exists: {0}")]
|
||||
AlreadyExists(String),
|
||||
|
||||
/// Returned when a provided file path is invalid or malformed
|
||||
#[error("Invalid file path: {0}")]
|
||||
InvalidPath(String),
|
||||
|
||||
/// Returned when an operation is not supported by the current implementation
|
||||
#[error("Operation not supported: {0}")]
|
||||
OperationNotSupported(String),
|
||||
|
||||
/// Wraps standard I/O errors from the filesystem
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
/// Indicates errors in the path-to-ID mapping system
|
||||
#[error("Mapping error: {0}")]
|
||||
MappingError(String),
|
||||
|
||||
/// Specific errors related to ID mapping operations
|
||||
#[error("ID Mapping error: {0}")]
|
||||
IdMappingError(String),
|
||||
|
||||
/// Returned when an operation exceeds its timeout threshold
|
||||
#[error("Timeout error: {0}")]
|
||||
Timeout(String),
|
||||
|
||||
/// Propagates domain model errors to the repository layer
|
||||
#[error("Domain error: {0}")]
|
||||
DomainError(#[from] DomainError),
|
||||
|
||||
/// Catch-all for other unspecified errors
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
// ─────────────────────────────────────────────────────
|
||||
// FileReadRepository — operaciones de lectura/consulta
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/// Puerto del dominio para **lectura** de archivos.
|
||||
///
|
||||
/// Encapsula toda operación que consulta estado sin modificarlo:
|
||||
/// obtener, listar, contenido, stream, mmap, rango, resolución de rutas.
|
||||
#[async_trait]
|
||||
pub trait FileReadRepository: Send + Sync + 'static {
|
||||
/// Obtiene un archivo por su ID.
|
||||
async fn get_file(&self, id: &str) -> Result<File, DomainError>;
|
||||
|
||||
/// Lista archivos en una carpeta.
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>;
|
||||
|
||||
/// Obtiene contenido completo como bytes (solo archivos pequeños/medianos).
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
||||
|
||||
/// Obtiene contenido como stream (ideal para archivos grandes).
|
||||
async fn get_file_stream(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||
|
||||
/// Stream de un rango de bytes (HTTP Range Requests, video seek).
|
||||
async fn get_file_range_stream(
|
||||
&self,
|
||||
id: &str,
|
||||
start: u64,
|
||||
end: Option<u64>,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||
|
||||
/// Memory-map de archivo para acceso zero-copy (10–100 MB).
|
||||
async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError>;
|
||||
|
||||
/// Obtiene la ruta de almacenamiento lógica de un archivo.
|
||||
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||
|
||||
/// Obtiene el ID de la carpeta padre a partir de una ruta (WebDAV).
|
||||
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type alias for results of file repository operations.
|
||||
*
|
||||
* Provides a consistent return type for all repository methods, containing
|
||||
* either a successful value or a FileRepositoryError.
|
||||
*/
|
||||
pub type FileRepositoryResult<T> = Result<T, FileRepositoryError>;
|
||||
// ─────────────────────────────────────────────────────
|
||||
// FileWriteRepository — operaciones de escritura/mutación
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Repository interface defining all file storage operations.
|
||||
*
|
||||
* This trait represents the primary port for file operations in the domain model,
|
||||
* following the hexagonal architecture pattern. It defines the contract that any
|
||||
* file storage implementation must fulfill, abstracting away implementation details
|
||||
* like filesystem specifics, cloud storage, or database operations.
|
||||
*
|
||||
* All implementations must be thread-safe (Send + Sync) and have a 'static lifetime
|
||||
* to support the async operations in the system.
|
||||
*/
|
||||
/// Puerto del dominio para **escritura** de archivos.
|
||||
///
|
||||
/// Cubre: upload (buffered + streaming), move, delete, update,
|
||||
/// y el registro diferido para write-behind cache.
|
||||
#[async_trait]
|
||||
pub trait FileRepository: Send + Sync + 'static {
|
||||
/**
|
||||
* Creates and saves a new file from binary content.
|
||||
*
|
||||
* This method handles new file creation with automatic ID generation,
|
||||
* content storage, and metadata registration.
|
||||
*
|
||||
* @param name The filename with extension
|
||||
* @param folder_id Optional ID of parent folder, None for root
|
||||
* @param content_type MIME type of the file
|
||||
* @param content Binary data of the file
|
||||
* @return A File entity with generated metadata on success, error otherwise
|
||||
*/
|
||||
async fn save_file_from_bytes(
|
||||
pub trait FileWriteRepository: Send + Sync + 'static {
|
||||
/// Guarda un nuevo archivo desde bytes.
|
||||
async fn save_file(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> FileRepositoryResult<File>;
|
||||
|
||||
/**
|
||||
* Creates and saves a new file from a stream of bytes.
|
||||
*
|
||||
* STREAMING UPLOAD: Writes chunks directly to disk as they arrive,
|
||||
* avoiding memory accumulation for large files. This is the preferred
|
||||
* method for handling uploads of any size.
|
||||
*
|
||||
* Benefits:
|
||||
* - Constant memory usage regardless of file size
|
||||
* - Faster time-to-first-byte for large files
|
||||
* - Better handling of slow network connections
|
||||
*
|
||||
* @param name The filename with extension
|
||||
* @param folder_id Optional ID of parent folder, None for root
|
||||
* @param content_type MIME type of the file
|
||||
* @param stream Async stream of byte chunks
|
||||
* @return A File entity with generated metadata on success, error otherwise
|
||||
*/
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Upload en streaming — escribe chunks a disco sin acumular en RAM.
|
||||
async fn save_file_from_stream(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
||||
) -> FileRepositoryResult<File>;
|
||||
|
||||
/**
|
||||
* Saves a file with a predetermined ID.
|
||||
*
|
||||
* Similar to save_file_from_bytes but allows specifying the ID,
|
||||
* useful for restoring files or migrations.
|
||||
*
|
||||
* @param id Predefined unique ID for the file
|
||||
* @param name The filename with extension
|
||||
* @param folder_id Optional ID of parent folder, None for root
|
||||
* @param content_type MIME type of the file
|
||||
* @param content Binary data of the file
|
||||
* @return The created File entity on success, error otherwise
|
||||
*/
|
||||
async fn save_file_with_id(
|
||||
stream: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Mueve un archivo a otra carpeta.
|
||||
async fn move_file(
|
||||
&self,
|
||||
file_id: &str,
|
||||
target_folder_id: Option<String>,
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Elimina un archivo.
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Actualiza el contenido de un archivo existente.
|
||||
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError>;
|
||||
|
||||
/// Registra metadatos de archivo SIN escribir contenido a disco (write-behind).
|
||||
///
|
||||
/// Devuelve `(File, PathBuf)` donde `PathBuf` es la ruta destino para la
|
||||
/// escritura diferida que realizará el `WriteBehindCache`.
|
||||
async fn register_file_deferred(
|
||||
&self,
|
||||
id: String,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> FileRepositoryResult<File>;
|
||||
|
||||
/**
|
||||
* Retrieves a file entity by its unique ID.
|
||||
*
|
||||
* @param id The unique identifier of the file
|
||||
* @return The File entity if found, NotFound error otherwise
|
||||
*/
|
||||
async fn get_file_by_id(&self, id: &str) -> FileRepositoryResult<File>;
|
||||
|
||||
/**
|
||||
* Lists all files within a specified folder.
|
||||
*
|
||||
* @param folder_id Optional folder ID to list files from, None for root
|
||||
* @return Vector of File entities in the folder
|
||||
*/
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> FileRepositoryResult<Vec<File>>;
|
||||
|
||||
/**
|
||||
* Deletes a file by ID.
|
||||
*
|
||||
* @param id The unique identifier of the file to delete
|
||||
* @return Success or error
|
||||
*/
|
||||
async fn delete_file(&self, id: &str) -> FileRepositoryResult<()>;
|
||||
|
||||
/**
|
||||
* Deletes a file and removes its mapping entries.
|
||||
*
|
||||
* More thorough than delete_file as it also purges ID mappings,
|
||||
* useful for permanent deletions.
|
||||
*
|
||||
* @param id The unique identifier of the file to delete
|
||||
* @return Success or error
|
||||
*/
|
||||
async fn delete_file_entry(&self, id: &str) -> FileRepositoryResult<()>;
|
||||
|
||||
/**
|
||||
* Retrieves the complete file content as a byte vector.
|
||||
*
|
||||
* This method loads the entire file into memory, so it should
|
||||
* only be used for reasonably sized files.
|
||||
*
|
||||
* @param id The unique identifier of the file
|
||||
* @return The file's binary content
|
||||
*/
|
||||
async fn get_file_content(&self, id: &str) -> FileRepositoryResult<Vec<u8>>;
|
||||
|
||||
/**
|
||||
* Retrieves file content as an asynchronous stream of bytes.
|
||||
*
|
||||
* Preferred for large files as it avoids loading everything into memory at once.
|
||||
*
|
||||
* @param id The unique identifier of the file
|
||||
* @return A stream that yields chunks of file data
|
||||
*/
|
||||
#[allow(clippy::type_complexity)]
|
||||
async fn get_file_stream(&self, id: &str) -> FileRepositoryResult<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>;
|
||||
|
||||
/**
|
||||
* Retrieves a range of file content as an asynchronous stream.
|
||||
*
|
||||
* Used for HTTP Range Requests to support:
|
||||
* - Video seeking
|
||||
* - Resumable downloads
|
||||
* - Parallel chunk downloads
|
||||
*
|
||||
* @param id The unique identifier of the file
|
||||
* @param start Starting byte position (inclusive)
|
||||
* @param end Ending byte position (inclusive), None means until EOF
|
||||
* @return A stream that yields chunks of file data for the specified range
|
||||
*/
|
||||
#[allow(clippy::type_complexity)]
|
||||
async fn get_file_range_stream(
|
||||
&self,
|
||||
id: &str,
|
||||
start: u64,
|
||||
end: Option<u64>
|
||||
) -> FileRepositoryResult<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>;
|
||||
|
||||
/**
|
||||
* Memory-maps a file for zero-copy access.
|
||||
*
|
||||
* Uses memory-mapped I/O for efficient reading of medium-sized files (10-100MB).
|
||||
* The kernel handles page faults and caching, providing near-zero-copy performance.
|
||||
*
|
||||
* IMPORTANT: This is synchronous I/O wrapped in spawn_blocking.
|
||||
* Best for files that will be read sequentially in full.
|
||||
*
|
||||
* @param id The unique identifier of the file
|
||||
* @return Bytes containing the memory-mapped file content
|
||||
*/
|
||||
async fn get_file_mmap(&self, id: &str) -> FileRepositoryResult<Bytes>;
|
||||
|
||||
/**
|
||||
* Moves a file to a different folder.
|
||||
*
|
||||
* @param id The unique identifier of the file to move
|
||||
* @param target_folder_id The destination folder ID, None for root
|
||||
* @return The updated File entity after the move
|
||||
*/
|
||||
async fn move_file(&self, id: &str, target_folder_id: Option<String>) -> FileRepositoryResult<File>;
|
||||
|
||||
/**
|
||||
* Retrieves the storage path for a file.
|
||||
*
|
||||
* @param id The unique identifier of the file
|
||||
* @return The StoragePath object representing the file's location
|
||||
*/
|
||||
async fn get_file_path(&self, id: &str) -> FileRepositoryResult<StoragePath>;
|
||||
|
||||
/**
|
||||
* Moves a file to the trash system.
|
||||
*
|
||||
* Instead of permanent deletion, this marks the file as trashed
|
||||
* and relocates it to the trash storage area.
|
||||
*
|
||||
* @param file_id The unique identifier of the file to trash
|
||||
* @return Success or error
|
||||
*/
|
||||
async fn move_to_trash(&self, file_id: &str) -> FileRepositoryResult<()>;
|
||||
|
||||
/**
|
||||
* Restores a file from the trash to its original location.
|
||||
*
|
||||
* @param file_id The unique identifier of the file to restore
|
||||
* @param original_path The original path where the file was located before trashing
|
||||
* @return Success or error
|
||||
*/
|
||||
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> FileRepositoryResult<()>;
|
||||
|
||||
/**
|
||||
* Permanently deletes a file from the trash system.
|
||||
*
|
||||
* This operation is not reversible and removes the file completely.
|
||||
* Used primarily by the trash cleanup service.
|
||||
*
|
||||
* @param file_id The unique identifier of the file to permanently delete
|
||||
* @return Success or error
|
||||
*/
|
||||
async fn delete_file_permanently(&self, file_id: &str) -> FileRepositoryResult<()>;
|
||||
|
||||
/**
|
||||
* Updates the content of an existing file.
|
||||
*
|
||||
* This method replaces the binary content of a file while preserving its
|
||||
* metadata like ID, creation timestamp, and location.
|
||||
*
|
||||
* @param file_id The unique identifier of the file to update
|
||||
* @param content The new binary content for the file
|
||||
* @return Success or error
|
||||
*/
|
||||
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> FileRepositoryResult<()>;
|
||||
}
|
||||
size: u64,
|
||||
) -> Result<(File, PathBuf), DomainError>;
|
||||
|
||||
// ── Trash operations ──
|
||||
|
||||
/// Mueve un archivo a la papelera
|
||||
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Restaura un archivo desde la papelera a su ubicación original
|
||||
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Elimina un archivo permanentemente (usado por la papelera)
|
||||
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// FileRepository — supertrait unificado
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/// Puerto unificado para persistencia de archivos.
|
||||
///
|
||||
/// Es un supertrait de `FileReadRepository + FileWriteRepository`.
|
||||
/// Cualquier tipo que implemente ambos ports obtiene `FileRepository`
|
||||
/// automáticamente vía blanket impl.
|
||||
pub trait FileRepository: FileReadRepository + FileWriteRepository {}
|
||||
|
||||
/// Blanket implementation: cualquier tipo que implemente ambos ports
|
||||
/// es automáticamente un FileRepository.
|
||||
impl<T: FileReadRepository + FileWriteRepository> FileRepository for T {}
|
||||
|
||||
@@ -1,100 +1,69 @@
|
||||
//! Puerto de persistencia del dominio para la entidad Folder.
|
||||
//!
|
||||
//! Define el contrato que cualquier implementación de almacenamiento de carpetas
|
||||
//! debe cumplir. Este trait vive en el dominio porque Folder es una entidad core
|
||||
//! del sistema y sus contratos de persistencia pertenecen a la capa de dominio,
|
||||
//! siguiendo los principios de Clean/Hexagonal Architecture.
|
||||
//!
|
||||
//! Las implementaciones concretas (filesystem, PostgreSQL, S3, etc.) viven en
|
||||
//! la capa de infraestructura.
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::domain::entities::folder::Folder;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Error types for folder repository operations
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
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("IO Error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Mapping error: {0}")]
|
||||
MappingError(String),
|
||||
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(String),
|
||||
|
||||
#[error("Domain error: {0}")]
|
||||
DomainError(#[from] DomainError),
|
||||
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
/// Result type for folder repository operations
|
||||
pub type FolderRepositoryResult<T> = Result<T, FolderRepositoryError>;
|
||||
|
||||
/// Repository interface for folder operations (primary port)
|
||||
/// Puerto del dominio para persistencia de carpetas.
|
||||
///
|
||||
/// Define las operaciones CRUD y de gestión necesarias para
|
||||
/// la entidad Folder en el sistema de almacenamiento.
|
||||
#[async_trait]
|
||||
pub trait FolderRepository: Send + Sync + 'static {
|
||||
/// Creates a new folder
|
||||
async fn create_folder(&self, name: String, parent_id: Option<String>) -> FolderRepositoryResult<Folder>;
|
||||
/// Crea una nueva carpeta
|
||||
async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Gets a folder by its ID
|
||||
async fn get_folder_by_id(&self, id: &str) -> FolderRepositoryResult<Folder>;
|
||||
/// Obtiene una carpeta por su ID
|
||||
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Gets a folder by its path
|
||||
async fn get_folder_by_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult<Folder>;
|
||||
/// Obtiene una carpeta por su ruta de almacenamiento
|
||||
async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Lists all folders in a parent folder (use with caution for large directories)
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> FolderRepositoryResult<Vec<Folder>>;
|
||||
/// Lista carpetas dentro de una carpeta padre
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError>;
|
||||
|
||||
/// Lists folders in a parent folder with pagination support
|
||||
///
|
||||
/// * `parent_id` - Optional parent folder ID
|
||||
/// * `offset` - Number of folders to skip
|
||||
/// * `limit` - Maximum number of folders to return
|
||||
/// * `include_total` - If true, returns the total count of folders as well
|
||||
/// Lista carpetas con paginación
|
||||
async fn list_folders_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
offset: usize,
|
||||
parent_id: Option<&str>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
include_total: bool
|
||||
) -> FolderRepositoryResult<(Vec<Folder>, Option<usize>)>;
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
|
||||
|
||||
/// Renames a folder
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> FolderRepositoryResult<Folder>;
|
||||
/// Renombra una carpeta
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Moves a folder to a new parent
|
||||
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> FolderRepositoryResult<Folder>;
|
||||
/// Mueve una carpeta a otro padre
|
||||
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Deletes a folder
|
||||
async fn delete_folder(&self, id: &str) -> FolderRepositoryResult<()>;
|
||||
/// Elimina una carpeta
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Checks if a folder exists at the given path
|
||||
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult<bool>;
|
||||
/// Verifica si existe una carpeta en la ruta dada
|
||||
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||
|
||||
/// Gets the storage path for a folder
|
||||
async fn get_folder_storage_path(&self, id: &str) -> FolderRepositoryResult<StoragePath>;
|
||||
|
||||
/// Legacy method - checks if a folder exists at the given PathBuf path
|
||||
#[deprecated(note = "Use folder_exists_at_storage_path instead")]
|
||||
async fn folder_exists(&self, path: &std::path::PathBuf) -> FolderRepositoryResult<bool>;
|
||||
|
||||
/// Legacy method - gets a folder by its PathBuf path
|
||||
#[deprecated(note = "Use get_folder_by_storage_path instead")]
|
||||
async fn get_folder_by_path(&self, path: &std::path::PathBuf) -> FolderRepositoryResult<Folder>;
|
||||
|
||||
/// Moves a folder to trash
|
||||
async fn move_to_trash(&self, folder_id: &str) -> FolderRepositoryResult<()>;
|
||||
|
||||
/// Restores a folder from trash
|
||||
async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> FolderRepositoryResult<()>;
|
||||
|
||||
/// Permanently deletes a folder (used for trash cleanup)
|
||||
async fn delete_folder_permanently(&self, folder_id: &str) -> FolderRepositoryResult<()>;
|
||||
}
|
||||
/// Obtiene la ruta de una carpeta
|
||||
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||
|
||||
// ── Trash operations ──
|
||||
|
||||
/// Mueve una carpeta a la papelera
|
||||
async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Restaura una carpeta desde la papelera a su ubicación original
|
||||
async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Elimina una carpeta permanentemente (usado por la papelera)
|
||||
async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user