style: apply cargo fmt to entire codebase

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

This establishes a consistent code style baseline for the
project going forward.
This commit is contained in:
Dionisio
2026-02-14 01:29:34 +01:00
parent 67137a3ef2
commit 4c98c5a657
173 changed files with 23368 additions and 17590 deletions
File diff suppressed because it is too large Load Diff
+339 -286
View File
@@ -1,286 +1,339 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::io::Cursor; use crate::application::adapters::caldav_adapter::{CalDavAdapter, CalDavReportType};
use std::collections::HashMap; use crate::application::adapters::webdav_adapter::{
use chrono::{Utc, TimeZone}; PropFindRequest, PropFindType, QualifiedName,
use crate::application::adapters::caldav_adapter::{CalDavAdapter, CalDavReportType}; };
use crate::application::adapters::webdav_adapter::{PropFindRequest, PropFindType, QualifiedName}; use crate::application::dtos::calendar_dto::{CalendarDto, CalendarEventDto};
use crate::application::dtos::calendar_dto::{CalendarDto, CalendarEventDto}; use chrono::{TimeZone, Utc};
use std::collections::HashMap;
fn sample_calendar() -> CalendarDto { use std::io::Cursor;
CalendarDto {
id: "cal-001".to_string(), fn sample_calendar() -> CalendarDto {
name: "Personal".to_string(), CalendarDto {
owner_id: "user-001".to_string(), id: "cal-001".to_string(),
description: Some("My personal calendar".to_string()), name: "Personal".to_string(),
color: Some("#FF0000".to_string()), owner_id: "user-001".to_string(),
is_public: false, description: Some("My personal calendar".to_string()),
created_at: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(), color: Some("#FF0000".to_string()),
updated_at: Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap(), is_public: false,
custom_properties: HashMap::new(), created_at: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
} updated_at: Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap(),
} custom_properties: HashMap::new(),
}
fn sample_event() -> CalendarEventDto { }
CalendarEventDto {
id: "evt-001".to_string(), fn sample_event() -> CalendarEventDto {
calendar_id: "cal-001".to_string(), CalendarEventDto {
summary: "Team Meeting".to_string(), id: "evt-001".to_string(),
description: Some("Weekly team sync".to_string()), calendar_id: "cal-001".to_string(),
location: Some("Conference Room A".to_string()), summary: "Team Meeting".to_string(),
start_time: Utc.with_ymd_and_hms(2025, 6, 15, 10, 0, 0).unwrap(), description: Some("Weekly team sync".to_string()),
end_time: Utc.with_ymd_and_hms(2025, 6, 15, 11, 0, 0).unwrap(), location: Some("Conference Room A".to_string()),
all_day: false, start_time: Utc.with_ymd_and_hms(2025, 6, 15, 10, 0, 0).unwrap(),
rrule: None, end_time: Utc.with_ymd_and_hms(2025, 6, 15, 11, 0, 0).unwrap(),
ical_uid: "uid-evt-001@oxicloud".to_string(), all_day: false,
created_at: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(), rrule: None,
updated_at: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(), ical_uid: "uid-evt-001@oxicloud".to_string(),
} created_at: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
} updated_at: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
}
// ======================== }
// MKCALENDAR parsing tests
// ======================== // ========================
// MKCALENDAR parsing tests
#[test] // ========================
fn test_parse_mkcalendar_full() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?> #[test]
<C:mkcalendar xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> fn test_parse_mkcalendar_full() {
<D:set> let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<D:prop> <C:mkcalendar xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:displayname>Work Calendar</D:displayname> <D:set>
<C:calendar-description>Work related events</C:calendar-description> <D:prop>
<A:calendar-color xmlns:A="http://apple.com/ns/ical/">#0000FF</A:calendar-color> <D:displayname>Work Calendar</D:displayname>
</D:prop> <C:calendar-description>Work related events</C:calendar-description>
</D:set> <A:calendar-color xmlns:A="http://apple.com/ns/ical/">#0000FF</A:calendar-color>
</C:mkcalendar>"#; </D:prop>
</D:set>
let result = CalDavAdapter::parse_mkcalendar(Cursor::new(xml)); </C:mkcalendar>"#;
assert!(result.is_ok(), "Failed to parse MKCALENDAR: {:?}", result.err());
let (name, desc, color) = result.unwrap(); let result = CalDavAdapter::parse_mkcalendar(Cursor::new(xml));
assert_eq!(name, "Work Calendar"); assert!(
assert_eq!(desc, Some("Work related events".to_string())); result.is_ok(),
assert_eq!(color, Some("#0000FF".to_string())); "Failed to parse MKCALENDAR: {:?}",
} result.err()
);
#[test] let (name, desc, color) = result.unwrap();
fn test_parse_mkcalendar_name_only() { assert_eq!(name, "Work Calendar");
let xml = r#"<?xml version="1.0" encoding="UTF-8"?> assert_eq!(desc, Some("Work related events".to_string()));
<C:mkcalendar xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> assert_eq!(color, Some("#0000FF".to_string()));
<D:set> }
<D:prop>
<D:displayname>Minimal Calendar</D:displayname> #[test]
</D:prop> fn test_parse_mkcalendar_name_only() {
</D:set> let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
</C:mkcalendar>"#; <C:mkcalendar xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:set>
let result = CalDavAdapter::parse_mkcalendar(Cursor::new(xml)); <D:prop>
assert!(result.is_ok()); <D:displayname>Minimal Calendar</D:displayname>
let (name, desc, color) = result.unwrap(); </D:prop>
assert_eq!(name, "Minimal Calendar"); </D:set>
assert!(desc.is_none()); </C:mkcalendar>"#;
assert!(color.is_none());
} let result = CalDavAdapter::parse_mkcalendar(Cursor::new(xml));
assert!(result.is_ok());
// ======================== let (name, desc, color) = result.unwrap();
// REPORT parsing tests assert_eq!(name, "Minimal Calendar");
// ======================== assert!(desc.is_none());
assert!(color.is_none());
#[test] }
fn test_parse_calendar_query_report() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?> // ========================
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> // REPORT parsing tests
<D:prop> // ========================
<D:getetag/>
<C:calendar-data/> #[test]
</D:prop> fn test_parse_calendar_query_report() {
<C:filter> let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<C:comp-filter name="VCALENDAR"> <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<C:comp-filter name="VEVENT"> <D:prop>
<C:time-range start="2025-06-01T00:00:00Z" end="2025-07-01T00:00:00Z"/> <D:getetag/>
</C:comp-filter> <C:calendar-data/>
</C:comp-filter> </D:prop>
</C:filter> <C:filter>
</C:calendar-query>"#; <C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
let result = CalDavAdapter::parse_report(Cursor::new(xml)); <C:time-range start="2025-06-01T00:00:00Z" end="2025-07-01T00:00:00Z"/>
assert!(result.is_ok(), "Failed to parse report: {:?}", result.err()); </C:comp-filter>
</C:comp-filter>
match result.unwrap() { </C:filter>
CalDavReportType::CalendarQuery { time_range, props } => { </C:calendar-query>"#;
assert!(time_range.is_some(), "Time range should be parsed");
let (start, end) = time_range.unwrap(); let result = CalDavAdapter::parse_report(Cursor::new(xml));
assert_eq!(start, Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap()); assert!(result.is_ok(), "Failed to parse report: {:?}", result.err());
assert_eq!(end, Utc.with_ymd_and_hms(2025, 7, 1, 0, 0, 0).unwrap());
assert!(!props.is_empty(), "Props should not be empty"); match result.unwrap() {
} CalDavReportType::CalendarQuery { time_range, props } => {
other => panic!("Expected CalendarQuery, got {:?}", other), assert!(time_range.is_some(), "Time range should be parsed");
} let (start, end) = time_range.unwrap();
} assert_eq!(start, Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap());
assert_eq!(end, Utc.with_ymd_and_hms(2025, 7, 1, 0, 0, 0).unwrap());
#[test] assert!(!props.is_empty(), "Props should not be empty");
fn test_parse_calendar_multiget_report() { }
let xml = r#"<?xml version="1.0" encoding="UTF-8"?> other => panic!("Expected CalendarQuery, got {:?}", other),
<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> }
<D:prop> }
<D:getetag/>
<C:calendar-data/> #[test]
</D:prop> fn test_parse_calendar_multiget_report() {
<D:href>/caldav/cal-001/evt-001.ics</D:href> let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<D:href>/caldav/cal-001/evt-002.ics</D:href> <C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
</C:calendar-multiget>"#; <D:prop>
<D:getetag/>
let result = CalDavAdapter::parse_report(Cursor::new(xml)); <C:calendar-data/>
assert!(result.is_ok(), "Failed to parse multiget: {:?}", result.err()); </D:prop>
<D:href>/caldav/cal-001/evt-001.ics</D:href>
match result.unwrap() { <D:href>/caldav/cal-001/evt-002.ics</D:href>
CalDavReportType::CalendarMultiget { hrefs, props } => { </C:calendar-multiget>"#;
assert_eq!(hrefs.len(), 2);
assert_eq!(hrefs[0], "/caldav/cal-001/evt-001.ics"); let result = CalDavAdapter::parse_report(Cursor::new(xml));
assert_eq!(hrefs[1], "/caldav/cal-001/evt-002.ics"); assert!(
assert!(!props.is_empty()); result.is_ok(),
} "Failed to parse multiget: {:?}",
other => panic!("Expected CalendarMultiget, got {:?}", other), result.err()
} );
}
match result.unwrap() {
// ======================== CalDavReportType::CalendarMultiget { hrefs, props } => {
// PROPFIND response tests assert_eq!(hrefs.len(), 2);
// ======================== assert_eq!(hrefs[0], "/caldav/cal-001/evt-001.ics");
assert_eq!(hrefs[1], "/caldav/cal-001/evt-002.ics");
#[test] assert!(!props.is_empty());
fn test_generate_calendars_propfind_response() { }
let calendars = vec![sample_calendar()]; other => panic!("Expected CalendarMultiget, got {:?}", other),
let request = PropFindRequest { }
prop_find_type: PropFindType::AllProp, }
};
// ========================
let mut output = Vec::new(); // PROPFIND response tests
let result = CalDavAdapter::generate_calendars_propfind_response( // ========================
&mut output,
&calendars, #[test]
&request, fn test_generate_calendars_propfind_response() {
"/caldav/", let calendars = vec![sample_calendar()];
); let request = PropFindRequest {
prop_find_type: PropFindType::AllProp,
assert!(result.is_ok(), "Failed to generate propfind response: {:?}", result.err()); };
let xml_str = String::from_utf8(output).expect("Invalid UTF-8 in response"); let mut output = Vec::new();
assert!(xml_str.contains("multistatus"), "Response should contain multistatus element"); let result = CalDavAdapter::generate_calendars_propfind_response(
assert!(xml_str.contains("Personal"), "Response should contain calendar name"); &mut output,
assert!(xml_str.contains("cal-001"), "Response should contain calendar ID in href"); &calendars,
} &request,
"/caldav/",
#[test] );
fn test_generate_calendar_collection_propfind_depth_0() {
let calendar = sample_calendar(); assert!(
let events = vec![sample_event()]; result.is_ok(),
let request = PropFindRequest { "Failed to generate propfind response: {:?}",
prop_find_type: PropFindType::AllProp, result.err()
}; );
let mut output = Vec::new(); let xml_str = String::from_utf8(output).expect("Invalid UTF-8 in response");
let result = CalDavAdapter::generate_calendar_collection_propfind( assert!(
&mut output, xml_str.contains("multistatus"),
&calendar, "Response should contain multistatus element"
&events, );
&request, assert!(
"/caldav/cal-001", xml_str.contains("Personal"),
"0", "Response should contain calendar name"
); );
assert!(
assert!(result.is_ok(), "Failed to generate collection propfind: {:?}", result.err()); xml_str.contains("cal-001"),
"Response should contain calendar ID in href"
let xml_str = String::from_utf8(output).expect("Invalid UTF-8"); );
assert!(xml_str.contains("multistatus"), "Should have multistatus"); }
assert!(xml_str.contains("Personal"), "Should have calendar name");
// Depth 0 should NOT include individual event resources #[test]
} fn test_generate_calendar_collection_propfind_depth_0() {
let calendar = sample_calendar();
#[test] let events = vec![sample_event()];
fn test_generate_calendar_collection_propfind_depth_1() { let request = PropFindRequest {
let calendar = sample_calendar(); prop_find_type: PropFindType::AllProp,
let events = vec![sample_event()]; };
let request = PropFindRequest {
prop_find_type: PropFindType::AllProp, let mut output = Vec::new();
}; let result = CalDavAdapter::generate_calendar_collection_propfind(
&mut output,
let mut output = Vec::new(); &calendar,
let result = CalDavAdapter::generate_calendar_collection_propfind( &events,
&mut output, &request,
&calendar, "/caldav/cal-001",
&events, "0",
&request, );
"/caldav/cal-001",
"1", assert!(
); result.is_ok(),
"Failed to generate collection propfind: {:?}",
assert!(result.is_ok(), "Failed to generate depth-1 propfind: {:?}", result.err()); result.err()
);
let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
assert!(xml_str.contains("multistatus"), "Should have multistatus"); let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
assert!(xml_str.contains("Personal"), "Should have calendar name"); assert!(xml_str.contains("multistatus"), "Should have multistatus");
// Depth 1 should include event resources assert!(xml_str.contains("Personal"), "Should have calendar name");
assert!(xml_str.contains("evt-001"), "Depth 1 should include event resources"); // Depth 0 should NOT include individual event resources
} }
// ======================== #[test]
// Calendar events response tests fn test_generate_calendar_collection_propfind_depth_1() {
// ======================== let calendar = sample_calendar();
let events = vec![sample_event()];
#[test] let request = PropFindRequest {
fn test_generate_calendar_events_response() { prop_find_type: PropFindType::AllProp,
let events = vec![sample_event()]; };
let report = CalDavReportType::CalendarQuery {
time_range: None, let mut output = Vec::new();
props: vec![ let result = CalDavAdapter::generate_calendar_collection_propfind(
QualifiedName { &mut output,
namespace: "DAV:".to_string(), &calendar,
name: "getetag".to_string(), &events,
}, &request,
QualifiedName { "/caldav/cal-001",
namespace: "urn:ietf:params:xml:ns:caldav".to_string(), "1",
name: "calendar-data".to_string(), );
},
], assert!(
}; result.is_ok(),
"Failed to generate depth-1 propfind: {:?}",
let mut output = Vec::new(); result.err()
let result = CalDavAdapter::generate_calendar_events_response( );
&mut output,
&events, let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
&report, assert!(xml_str.contains("multistatus"), "Should have multistatus");
"/caldav/cal-001", assert!(xml_str.contains("Personal"), "Should have calendar name");
); // Depth 1 should include event resources
assert!(
assert!(result.is_ok(), "Failed to generate events response: {:?}", result.err()); xml_str.contains("evt-001"),
"Depth 1 should include event resources"
let xml_str = String::from_utf8(output).expect("Invalid UTF-8"); );
assert!(xml_str.contains("multistatus"), "Should have multistatus"); }
assert!(xml_str.contains("evt-001"), "Should reference event ID");
assert!(xml_str.contains("BEGIN:VCALENDAR"), "Should contain iCal data"); // ========================
assert!(xml_str.contains("VEVENT"), "Should contain VEVENT component"); // Calendar events response tests
assert!(xml_str.contains("Team Meeting"), "Should contain event summary"); // ========================
}
#[test]
#[test] fn test_generate_calendar_events_response() {
fn test_generate_empty_events_response() { let events = vec![sample_event()];
let events: Vec<CalendarEventDto> = vec![]; let report = CalDavReportType::CalendarQuery {
let report = CalDavReportType::CalendarQuery { time_range: None,
time_range: None, props: vec![
props: vec![], QualifiedName {
}; namespace: "DAV:".to_string(),
name: "getetag".to_string(),
let mut output = Vec::new(); },
let result = CalDavAdapter::generate_calendar_events_response( QualifiedName {
&mut output, namespace: "urn:ietf:params:xml:ns:caldav".to_string(),
&events, name: "calendar-data".to_string(),
&report, },
"/caldav/cal-001", ],
); };
assert!(result.is_ok(), "Empty events should still produce valid response"); let mut output = Vec::new();
let xml_str = String::from_utf8(output).expect("Invalid UTF-8"); let result = CalDavAdapter::generate_calendar_events_response(
assert!(xml_str.contains("multistatus"), "Should have multistatus even for empty"); &mut output,
} &events,
} &report,
"/caldav/cal-001",
);
assert!(
result.is_ok(),
"Failed to generate events response: {:?}",
result.err()
);
let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
assert!(xml_str.contains("multistatus"), "Should have multistatus");
assert!(xml_str.contains("evt-001"), "Should reference event ID");
assert!(
xml_str.contains("BEGIN:VCALENDAR"),
"Should contain iCal data"
);
assert!(
xml_str.contains("VEVENT"),
"Should contain VEVENT component"
);
assert!(
xml_str.contains("Team Meeting"),
"Should contain event summary"
);
}
#[test]
fn test_generate_empty_events_response() {
let events: Vec<CalendarEventDto> = vec![];
let report = CalDavReportType::CalendarQuery {
time_range: None,
props: vec![],
};
let mut output = Vec::new();
let result = CalDavAdapter::generate_calendar_events_response(
&mut output,
&events,
&report,
"/caldav/cal-001",
);
assert!(
result.is_ok(),
"Empty events should still produce valid response"
);
let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
assert!(
xml_str.contains("multistatus"),
"Should have multistatus even for empty"
);
}
}
File diff suppressed because it is too large Load Diff
+524 -432
View File
@@ -1,432 +1,524 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::io::Cursor; use crate::application::adapters::carddav_adapter::{
use chrono::{Utc, TimeZone, NaiveDate}; CardDavAdapter, CardDavReportType, contact_to_vcard,
use crate::application::adapters::carddav_adapter::{CardDavAdapter, CardDavReportType, contact_to_vcard}; };
use crate::application::adapters::webdav_adapter::{PropFindRequest, PropFindType, QualifiedName}; use crate::application::adapters::webdav_adapter::{
use crate::application::dtos::address_book_dto::AddressBookDto; PropFindRequest, PropFindType, QualifiedName,
use crate::application::dtos::contact_dto::{ContactDto, EmailDto, PhoneDto, AddressDto}; };
use crate::application::dtos::address_book_dto::AddressBookDto;
fn sample_address_book() -> AddressBookDto { use crate::application::dtos::contact_dto::{AddressDto, ContactDto, EmailDto, PhoneDto};
AddressBookDto { use chrono::{NaiveDate, TimeZone, Utc};
id: "ab-001".to_string(), use std::io::Cursor;
name: "My Contacts".to_string(),
owner_id: "user-001".to_string(), fn sample_address_book() -> AddressBookDto {
description: Some("Personal address book".to_string()), AddressBookDto {
color: Some("#00FF00".to_string()), id: "ab-001".to_string(),
is_public: false, name: "My Contacts".to_string(),
created_at: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(), owner_id: "user-001".to_string(),
updated_at: Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap(), description: Some("Personal address book".to_string()),
} color: Some("#00FF00".to_string()),
} is_public: false,
created_at: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
fn sample_contact() -> ContactDto { updated_at: Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap(),
ContactDto { }
id: "contact-001".to_string(), }
address_book_id: "ab-001".to_string(),
uid: "uid-contact-001@oxicloud".to_string(), fn sample_contact() -> ContactDto {
full_name: Some("John Doe".to_string()), ContactDto {
first_name: Some("John".to_string()), id: "contact-001".to_string(),
last_name: Some("Doe".to_string()), address_book_id: "ab-001".to_string(),
nickname: Some("Johnny".to_string()), uid: "uid-contact-001@oxicloud".to_string(),
email: vec![ full_name: Some("John Doe".to_string()),
EmailDto { first_name: Some("John".to_string()),
email: "john@example.com".to_string(), last_name: Some("Doe".to_string()),
r#type: "work".to_string(), nickname: Some("Johnny".to_string()),
is_primary: true, email: vec![
}, EmailDto {
EmailDto { email: "john@example.com".to_string(),
email: "john.doe@personal.com".to_string(), r#type: "work".to_string(),
r#type: "home".to_string(), is_primary: true,
is_primary: false, },
}, EmailDto {
], email: "john.doe@personal.com".to_string(),
phone: vec![ r#type: "home".to_string(),
PhoneDto { is_primary: false,
number: "+1-555-0100".to_string(), },
r#type: "cell".to_string(), ],
is_primary: true, phone: vec![PhoneDto {
}, number: "+1-555-0100".to_string(),
], r#type: "cell".to_string(),
address: vec![ is_primary: true,
AddressDto { }],
street: Some("123 Main St".to_string()), address: vec![AddressDto {
city: Some("Springfield".to_string()), street: Some("123 Main St".to_string()),
state: Some("IL".to_string()), city: Some("Springfield".to_string()),
postal_code: Some("62701".to_string()), state: Some("IL".to_string()),
country: Some("US".to_string()), postal_code: Some("62701".to_string()),
r#type: "home".to_string(), country: Some("US".to_string()),
is_primary: true, r#type: "home".to_string(),
}, is_primary: true,
], }],
organization: Some("Acme Corp".to_string()), organization: Some("Acme Corp".to_string()),
title: Some("Software Engineer".to_string()), title: Some("Software Engineer".to_string()),
notes: Some("Met at conference".to_string()), notes: Some("Met at conference".to_string()),
photo_url: None, photo_url: None,
birthday: Some(NaiveDate::from_ymd_opt(1990, 5, 15).unwrap()), birthday: Some(NaiveDate::from_ymd_opt(1990, 5, 15).unwrap()),
anniversary: None, anniversary: None,
created_at: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(), created_at: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
updated_at: Utc.with_ymd_and_hms(2025, 3, 10, 8, 30, 0).unwrap(), updated_at: Utc.with_ymd_and_hms(2025, 3, 10, 8, 30, 0).unwrap(),
etag: "etag-abc123".to_string(), etag: "etag-abc123".to_string(),
} }
} }
fn sample_contact_minimal() -> ContactDto { fn sample_contact_minimal() -> ContactDto {
ContactDto { ContactDto {
id: "contact-002".to_string(), id: "contact-002".to_string(),
address_book_id: "ab-001".to_string(), address_book_id: "ab-001".to_string(),
uid: "uid-contact-002@oxicloud".to_string(), uid: "uid-contact-002@oxicloud".to_string(),
full_name: Some("Jane Smith".to_string()), full_name: Some("Jane Smith".to_string()),
first_name: None, first_name: None,
last_name: None, last_name: None,
nickname: None, nickname: None,
email: vec![], email: vec![],
phone: vec![], phone: vec![],
address: vec![], address: vec![],
organization: None, organization: None,
title: None, title: None,
notes: None, notes: None,
photo_url: None, photo_url: None,
birthday: None, birthday: None,
anniversary: None, anniversary: None,
created_at: Utc.with_ymd_and_hms(2025, 2, 1, 0, 0, 0).unwrap(), created_at: Utc.with_ymd_and_hms(2025, 2, 1, 0, 0, 0).unwrap(),
updated_at: Utc.with_ymd_and_hms(2025, 2, 1, 0, 0, 0).unwrap(), updated_at: Utc.with_ymd_and_hms(2025, 2, 1, 0, 0, 0).unwrap(),
etag: "etag-def456".to_string(), etag: "etag-def456".to_string(),
} }
} }
// ======================== // ========================
// vCard generation tests // vCard generation tests
// ======================== // ========================
#[test] #[test]
fn test_contact_to_vcard_full() { fn test_contact_to_vcard_full() {
let contact = sample_contact(); let contact = sample_contact();
let vcard = contact_to_vcard(&contact); let vcard = contact_to_vcard(&contact);
assert!(vcard.starts_with("BEGIN:VCARD"), "vCard should start with BEGIN:VCARD"); assert!(
assert!(vcard.contains("VERSION:3.0"), "Should be vCard 3.0"); vcard.starts_with("BEGIN:VCARD"),
assert!(vcard.contains("FN:John Doe"), "Should contain full name"); "vCard should start with BEGIN:VCARD"
assert!(vcard.contains("N:Doe;John"), "Should contain structured name"); );
assert!(vcard.contains("NICKNAME:Johnny"), "Should contain nickname"); assert!(vcard.contains("VERSION:3.0"), "Should be vCard 3.0");
assert!(vcard.contains("john@example.com"), "Should contain email"); assert!(vcard.contains("FN:John Doe"), "Should contain full name");
assert!(vcard.contains("+1-555-0100"), "Should contain phone number"); assert!(
assert!(vcard.contains("ORG:Acme Corp"), "Should contain organization"); vcard.contains("N:Doe;John"),
assert!(vcard.contains("TITLE:Software Engineer"), "Should contain title"); "Should contain structured name"
assert!(vcard.contains("NOTE:Met at conference"), "Should contain notes"); );
assert!(vcard.contains("BDAY:1990-05-15"), "Should contain birthday"); assert!(vcard.contains("NICKNAME:Johnny"), "Should contain nickname");
assert!(vcard.contains("UID:uid-contact-001@oxicloud"), "Should contain UID"); assert!(vcard.contains("john@example.com"), "Should contain email");
assert!(vcard.ends_with("END:VCARD\r\n") || vcard.trim_end().ends_with("END:VCARD"), assert!(vcard.contains("+1-555-0100"), "Should contain phone number");
"vCard should end with END:VCARD"); assert!(
} vcard.contains("ORG:Acme Corp"),
"Should contain organization"
#[test] );
fn test_contact_to_vcard_minimal() { assert!(
let contact = sample_contact_minimal(); vcard.contains("TITLE:Software Engineer"),
let vcard = contact_to_vcard(&contact); "Should contain title"
);
assert!(vcard.contains("BEGIN:VCARD"), "Should start correctly"); assert!(
assert!(vcard.contains("VERSION:3.0"), "Should be vCard 3.0"); vcard.contains("NOTE:Met at conference"),
assert!(vcard.contains("FN:Jane Smith"), "Should have full name"); "Should contain notes"
assert!(vcard.contains("UID:uid-contact-002@oxicloud"), "Should have UID"); );
assert!(vcard.contains("END:VCARD"), "Should end correctly"); assert!(vcard.contains("BDAY:1990-05-15"), "Should contain birthday");
// Should NOT contain optional fields assert!(
assert!(!vcard.contains("NICKNAME:"), "Should not have nickname"); vcard.contains("UID:uid-contact-001@oxicloud"),
assert!(!vcard.contains("ORG:"), "Should not have org"); "Should contain UID"
assert!(!vcard.contains("TITLE:"), "Should not have title"); );
assert!(!vcard.contains("BDAY:"), "Should not have birthday"); assert!(
} vcard.ends_with("END:VCARD\r\n") || vcard.trim_end().ends_with("END:VCARD"),
"vCard should end with END:VCARD"
// ======================== );
// MKADDRESSBOOK parsing tests }
// ========================
#[test]
#[test] fn test_contact_to_vcard_minimal() {
fn test_parse_mkaddressbook_full() { let contact = sample_contact_minimal();
let xml = r#"<?xml version="1.0" encoding="UTF-8"?> let vcard = contact_to_vcard(&contact);
<D:mkcol xmlns:D="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
<D:set> assert!(vcard.contains("BEGIN:VCARD"), "Should start correctly");
<D:prop> assert!(vcard.contains("VERSION:3.0"), "Should be vCard 3.0");
<D:displayname>Work Contacts</D:displayname> assert!(vcard.contains("FN:Jane Smith"), "Should have full name");
<CR:addressbook-description>Colleagues and clients</CR:addressbook-description> assert!(
</D:prop> vcard.contains("UID:uid-contact-002@oxicloud"),
</D:set> "Should have UID"
</D:mkcol>"#; );
assert!(vcard.contains("END:VCARD"), "Should end correctly");
let result = CardDavAdapter::parse_mkaddressbook(Cursor::new(xml)); // Should NOT contain optional fields
assert!(result.is_ok(), "Failed to parse mkaddressbook: {:?}", result.err()); assert!(!vcard.contains("NICKNAME:"), "Should not have nickname");
let (name, desc, color) = result.unwrap(); assert!(!vcard.contains("ORG:"), "Should not have org");
assert_eq!(name, "Work Contacts"); assert!(!vcard.contains("TITLE:"), "Should not have title");
assert_eq!(desc, Some("Colleagues and clients".to_string())); assert!(!vcard.contains("BDAY:"), "Should not have birthday");
assert!(color.is_none()); }
}
// ========================
#[test] // MKADDRESSBOOK parsing tests
fn test_parse_mkaddressbook_name_only() { // ========================
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<D:mkcol xmlns:D="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav"> #[test]
<D:set> fn test_parse_mkaddressbook_full() {
<D:prop> let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<D:displayname>Simple Book</D:displayname> <D:mkcol xmlns:D="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
</D:prop> <D:set>
</D:set> <D:prop>
</D:mkcol>"#; <D:displayname>Work Contacts</D:displayname>
<CR:addressbook-description>Colleagues and clients</CR:addressbook-description>
let result = CardDavAdapter::parse_mkaddressbook(Cursor::new(xml)); </D:prop>
assert!(result.is_ok()); </D:set>
let (name, desc, color) = result.unwrap(); </D:mkcol>"#;
assert_eq!(name, "Simple Book");
assert!(desc.is_none()); let result = CardDavAdapter::parse_mkaddressbook(Cursor::new(xml));
assert!(color.is_none()); assert!(
} result.is_ok(),
"Failed to parse mkaddressbook: {:?}",
// ======================== result.err()
// REPORT parsing tests );
// ======================== let (name, desc, color) = result.unwrap();
assert_eq!(name, "Work Contacts");
#[test] assert_eq!(desc, Some("Colleagues and clients".to_string()));
fn test_parse_addressbook_query_report() { assert!(color.is_none());
let xml = r#"<?xml version="1.0" encoding="UTF-8"?> }
<CR:addressbook-query xmlns:D="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
<D:prop> #[test]
<D:getetag/> fn test_parse_mkaddressbook_name_only() {
<CR:address-data/> let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
</D:prop> <D:mkcol xmlns:D="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
</CR:addressbook-query>"#; <D:set>
<D:prop>
let result = CardDavAdapter::parse_report(Cursor::new(xml)); <D:displayname>Simple Book</D:displayname>
assert!(result.is_ok(), "Failed to parse addressbook-query: {:?}", result.err()); </D:prop>
</D:set>
match result.unwrap() { </D:mkcol>"#;
CardDavReportType::AddressbookQuery { props } => {
assert!(!props.is_empty(), "Props should not be empty"); let result = CardDavAdapter::parse_mkaddressbook(Cursor::new(xml));
} assert!(result.is_ok());
other => panic!("Expected AddressbookQuery, got {:?}", other), let (name, desc, color) = result.unwrap();
} assert_eq!(name, "Simple Book");
} assert!(desc.is_none());
assert!(color.is_none());
#[test] }
fn test_parse_addressbook_multiget_report() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?> // ========================
<CR:addressbook-multiget xmlns:D="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav"> // REPORT parsing tests
<D:prop> // ========================
<D:getetag/>
<CR:address-data/> #[test]
</D:prop> fn test_parse_addressbook_query_report() {
<D:href>/carddav/ab-001/contact-001.vcf</D:href> let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<D:href>/carddav/ab-001/contact-002.vcf</D:href> <CR:addressbook-query xmlns:D="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
<D:href>/carddav/ab-001/contact-003.vcf</D:href> <D:prop>
</CR:addressbook-multiget>"#; <D:getetag/>
<CR:address-data/>
let result = CardDavAdapter::parse_report(Cursor::new(xml)); </D:prop>
assert!(result.is_ok(), "Failed to parse multiget: {:?}", result.err()); </CR:addressbook-query>"#;
match result.unwrap() { let result = CardDavAdapter::parse_report(Cursor::new(xml));
CardDavReportType::AddressbookMultiget { hrefs, props } => { assert!(
assert_eq!(hrefs.len(), 3, "Should have 3 hrefs"); result.is_ok(),
assert_eq!(hrefs[0], "/carddav/ab-001/contact-001.vcf"); "Failed to parse addressbook-query: {:?}",
assert_eq!(hrefs[2], "/carddav/ab-001/contact-003.vcf"); result.err()
assert!(!props.is_empty()); );
}
other => panic!("Expected AddressbookMultiget, got {:?}", other), match result.unwrap() {
} CardDavReportType::AddressbookQuery { props } => {
} assert!(!props.is_empty(), "Props should not be empty");
}
// ======================== other => panic!("Expected AddressbookQuery, got {:?}", other),
// PROPFIND response tests }
// ======================== }
#[test] #[test]
fn test_generate_addressbooks_propfind_response() { fn test_parse_addressbook_multiget_report() {
let addressbooks = vec![sample_address_book()]; let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
let request = PropFindRequest { <CR:addressbook-multiget xmlns:D="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
prop_find_type: PropFindType::AllProp, <D:prop>
}; <D:getetag/>
<CR:address-data/>
let mut output = Vec::new(); </D:prop>
let result = CardDavAdapter::generate_addressbooks_propfind_response( <D:href>/carddav/ab-001/contact-001.vcf</D:href>
&mut output, <D:href>/carddav/ab-001/contact-002.vcf</D:href>
&addressbooks, <D:href>/carddav/ab-001/contact-003.vcf</D:href>
&request, </CR:addressbook-multiget>"#;
"/carddav",
); let result = CardDavAdapter::parse_report(Cursor::new(xml));
assert!(
assert!(result.is_ok(), "Failed to generate propfind response: {:?}", result.err()); result.is_ok(),
"Failed to parse multiget: {:?}",
let xml_str = String::from_utf8(output).expect("Invalid UTF-8"); result.err()
assert!(xml_str.contains("multistatus"), "Should contain multistatus"); );
assert!(xml_str.contains("My Contacts"), "Should contain address book name");
assert!(xml_str.contains("ab-001"), "Should contain address book ID in href"); match result.unwrap() {
} CardDavReportType::AddressbookMultiget { hrefs, props } => {
assert_eq!(hrefs.len(), 3, "Should have 3 hrefs");
#[test] assert_eq!(hrefs[0], "/carddav/ab-001/contact-001.vcf");
fn test_generate_addressbook_collection_propfind_depth_0() { assert_eq!(hrefs[2], "/carddav/ab-001/contact-003.vcf");
let addressbook = sample_address_book(); assert!(!props.is_empty());
let contacts = vec![sample_contact()]; }
let request = PropFindRequest { other => panic!("Expected AddressbookMultiget, got {:?}", other),
prop_find_type: PropFindType::AllProp, }
}; }
let mut output = Vec::new(); // ========================
let result = CardDavAdapter::generate_addressbook_collection_propfind( // PROPFIND response tests
&mut output, // ========================
&addressbook,
&contacts, #[test]
&request, fn test_generate_addressbooks_propfind_response() {
"/carddav/ab-001", let addressbooks = vec![sample_address_book()];
"0", let request = PropFindRequest {
); prop_find_type: PropFindType::AllProp,
};
assert!(result.is_ok(), "Failed to generate depth-0 propfind: {:?}", result.err());
let mut output = Vec::new();
let xml_str = String::from_utf8(output).expect("Invalid UTF-8"); let result = CardDavAdapter::generate_addressbooks_propfind_response(
assert!(xml_str.contains("multistatus"), "Should contain multistatus"); &mut output,
assert!(xml_str.contains("My Contacts"), "Should contain address book name"); &addressbooks,
} &request,
"/carddav",
#[test] );
fn test_generate_addressbook_collection_propfind_depth_1() {
let addressbook = sample_address_book(); assert!(
let contacts = vec![sample_contact(), sample_contact_minimal()]; result.is_ok(),
let request = PropFindRequest { "Failed to generate propfind response: {:?}",
prop_find_type: PropFindType::AllProp, result.err()
}; );
let mut output = Vec::new(); let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
let result = CardDavAdapter::generate_addressbook_collection_propfind( assert!(
&mut output, xml_str.contains("multistatus"),
&addressbook, "Should contain multistatus"
&contacts, );
&request, assert!(
"/carddav/ab-001", xml_str.contains("My Contacts"),
"1", "Should contain address book name"
); );
assert!(
assert!(result.is_ok(), "Failed to generate depth-1 propfind: {:?}", result.err()); xml_str.contains("ab-001"),
"Should contain address book ID in href"
let xml_str = String::from_utf8(output).expect("Invalid UTF-8"); );
assert!(xml_str.contains("multistatus"), "Should contain multistatus"); }
assert!(xml_str.contains("My Contacts"), "Should contain address book name");
// Depth 1 should include contact resources #[test]
assert!(xml_str.contains("contact-001"), "Should include contact-001"); fn test_generate_addressbook_collection_propfind_depth_0() {
assert!(xml_str.contains("contact-002"), "Should include contact-002"); let addressbook = sample_address_book();
} let contacts = vec![sample_contact()];
let request = PropFindRequest {
// ======================== prop_find_type: PropFindType::AllProp,
// Contacts response tests };
// ========================
let mut output = Vec::new();
#[test] let result = CardDavAdapter::generate_addressbook_collection_propfind(
fn test_generate_contacts_response() { &mut output,
let contacts = vec![sample_contact()]; &addressbook,
let vcards = vec![ &contacts,
("contact-001".to_string(), contact_to_vcard(&sample_contact())), &request,
]; "/carddav/ab-001",
let report = CardDavReportType::AddressbookQuery { "0",
props: vec![ );
QualifiedName {
namespace: "DAV:".to_string(), assert!(
name: "getetag".to_string(), result.is_ok(),
}, "Failed to generate depth-0 propfind: {:?}",
QualifiedName { result.err()
namespace: "urn:ietf:params:xml:ns:carddav".to_string(), );
name: "address-data".to_string(),
}, let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
], assert!(
}; xml_str.contains("multistatus"),
"Should contain multistatus"
let mut output = Vec::new(); );
let result = CardDavAdapter::generate_contacts_response( assert!(
&mut output, xml_str.contains("My Contacts"),
&contacts, "Should contain address book name"
&vcards, );
&report, }
"/carddav/ab-001",
); #[test]
fn test_generate_addressbook_collection_propfind_depth_1() {
assert!(result.is_ok(), "Failed to generate contacts response: {:?}", result.err()); let addressbook = sample_address_book();
let contacts = vec![sample_contact(), sample_contact_minimal()];
let xml_str = String::from_utf8(output).expect("Invalid UTF-8"); let request = PropFindRequest {
assert!(xml_str.contains("multistatus"), "Should contain multistatus"); prop_find_type: PropFindType::AllProp,
assert!(xml_str.contains("contact-001"), "Should reference contact"); };
assert!(xml_str.contains("etag-abc123"), "Should contain etag");
} let mut output = Vec::new();
let result = CardDavAdapter::generate_addressbook_collection_propfind(
#[test] &mut output,
fn test_generate_empty_contacts_response() { &addressbook,
let contacts: Vec<ContactDto> = vec![]; &contacts,
let vcards: Vec<(String, String)> = vec![]; &request,
let report = CardDavReportType::AddressbookQuery { "/carddav/ab-001",
props: vec![], "1",
}; );
let mut output = Vec::new(); assert!(
let result = CardDavAdapter::generate_contacts_response( result.is_ok(),
&mut output, "Failed to generate depth-1 propfind: {:?}",
&contacts, result.err()
&vcards, );
&report,
"/carddav/ab-001", let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
); assert!(
xml_str.contains("multistatus"),
assert!(result.is_ok(), "Empty contacts should produce valid response"); "Should contain multistatus"
let xml_str = String::from_utf8(output).expect("Invalid UTF-8"); );
assert!(xml_str.contains("multistatus"), "Should have multistatus"); assert!(
} xml_str.contains("My Contacts"),
"Should contain address book name"
// ======================== );
// Multiple address books test // Depth 1 should include contact resources
// ======================== assert!(
xml_str.contains("contact-001"),
#[test] "Should include contact-001"
fn test_generate_multiple_addressbooks() { );
let mut ab2 = sample_address_book(); assert!(
ab2.id = "ab-002".to_string(); xml_str.contains("contact-002"),
ab2.name = "Work Contacts".to_string(); "Should include contact-002"
);
let addressbooks = vec![sample_address_book(), ab2]; }
let request = PropFindRequest {
prop_find_type: PropFindType::AllProp, // ========================
}; // Contacts response tests
// ========================
let mut output = Vec::new();
let result = CardDavAdapter::generate_addressbooks_propfind_response( #[test]
&mut output, fn test_generate_contacts_response() {
&addressbooks, let contacts = vec![sample_contact()];
&request, let vcards = vec![(
"/carddav/", "contact-001".to_string(),
); contact_to_vcard(&sample_contact()),
)];
assert!(result.is_ok()); let report = CardDavReportType::AddressbookQuery {
let xml_str = String::from_utf8(output).expect("Invalid UTF-8"); props: vec![
assert!(xml_str.contains("My Contacts"), "Should contain first address book"); QualifiedName {
assert!(xml_str.contains("Work Contacts"), "Should contain second address book"); namespace: "DAV:".to_string(),
assert!(xml_str.contains("ab-001"), "Should have first ID"); name: "getetag".to_string(),
assert!(xml_str.contains("ab-002"), "Should have second ID"); },
} QualifiedName {
namespace: "urn:ietf:params:xml:ns:carddav".to_string(),
// ======================== name: "address-data".to_string(),
// vCard edge cases },
// ======================== ],
};
#[test]
fn test_contact_to_vcard_with_multiple_emails() { let mut output = Vec::new();
let contact = sample_contact(); let result = CardDavAdapter::generate_contacts_response(
let vcard = contact_to_vcard(&contact); &mut output,
&contacts,
// Should contain both emails &vcards,
assert!(vcard.contains("john@example.com"), "Should have work email"); &report,
assert!(vcard.contains("john.doe@personal.com"), "Should have personal email"); "/carddav/ab-001",
} );
#[test] assert!(
fn test_contact_to_vcard_address_formatting() { result.is_ok(),
let contact = sample_contact(); "Failed to generate contacts response: {:?}",
let vcard = contact_to_vcard(&contact); result.err()
);
// vCard ADR format: ;;street;city;state;postal;country
assert!(vcard.contains("123 Main St"), "Should have street"); let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
assert!(vcard.contains("Springfield"), "Should have city"); assert!(
assert!(vcard.contains("62701"), "Should have postal code"); xml_str.contains("multistatus"),
} "Should contain multistatus"
} );
assert!(xml_str.contains("contact-001"), "Should reference contact");
assert!(xml_str.contains("etag-abc123"), "Should contain etag");
}
#[test]
fn test_generate_empty_contacts_response() {
let contacts: Vec<ContactDto> = vec![];
let vcards: Vec<(String, String)> = vec![];
let report = CardDavReportType::AddressbookQuery { props: vec![] };
let mut output = Vec::new();
let result = CardDavAdapter::generate_contacts_response(
&mut output,
&contacts,
&vcards,
&report,
"/carddav/ab-001",
);
assert!(
result.is_ok(),
"Empty contacts should produce valid response"
);
let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
assert!(xml_str.contains("multistatus"), "Should have multistatus");
}
// ========================
// Multiple address books test
// ========================
#[test]
fn test_generate_multiple_addressbooks() {
let mut ab2 = sample_address_book();
ab2.id = "ab-002".to_string();
ab2.name = "Work Contacts".to_string();
let addressbooks = vec![sample_address_book(), ab2];
let request = PropFindRequest {
prop_find_type: PropFindType::AllProp,
};
let mut output = Vec::new();
let result = CardDavAdapter::generate_addressbooks_propfind_response(
&mut output,
&addressbooks,
&request,
"/carddav/",
);
assert!(result.is_ok());
let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
assert!(
xml_str.contains("My Contacts"),
"Should contain first address book"
);
assert!(
xml_str.contains("Work Contacts"),
"Should contain second address book"
);
assert!(xml_str.contains("ab-001"), "Should have first ID");
assert!(xml_str.contains("ab-002"), "Should have second ID");
}
// ========================
// vCard edge cases
// ========================
#[test]
fn test_contact_to_vcard_with_multiple_emails() {
let contact = sample_contact();
let vcard = contact_to_vcard(&contact);
// Should contain both emails
assert!(vcard.contains("john@example.com"), "Should have work email");
assert!(
vcard.contains("john.doe@personal.com"),
"Should have personal email"
);
}
#[test]
fn test_contact_to_vcard_address_formatting() {
let contact = sample_contact();
let vcard = contact_to_vcard(&contact);
// vCard ADR format: ;;street;city;state;postal;country
assert!(vcard.contains("123 Main St"), "Should have street");
assert!(vcard.contains("Springfield"), "Should have city");
assert!(vcard.contains("62701"), "Should have postal code");
}
}
+1 -1
View File
@@ -1,8 +1,8 @@
//! Adapters module for translating between external protocols and internal models //! Adapters module for translating between external protocols and internal models
pub mod webdav_adapter;
pub mod caldav_adapter; pub mod caldav_adapter;
pub mod carddav_adapter; pub mod carddav_adapter;
pub mod webdav_adapter;
#[cfg(test)] #[cfg(test)]
mod caldav_adapter_test; mod caldav_adapter_test;
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::domain::entities::contact::AddressBook;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::domain::entities::contact::AddressBook;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddressBookDto { pub struct AddressBookDto {
@@ -73,4 +73,4 @@ pub struct ShareAddressBookDto {
pub struct UnshareAddressBookDto { pub struct UnshareAddressBookDto {
pub address_book_id: String, pub address_book_id: String,
pub user_id: String, pub user_id: String,
} }
+4 -4
View File
@@ -1,8 +1,8 @@
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use crate::domain::entities::calendar::Calendar; use crate::domain::entities::calendar::Calendar;
use crate::domain::entities::calendar_event::CalendarEvent; use crate::domain::entities::calendar_event::CalendarEvent;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// DTO for calendar data transfer /// DTO for calendar data transfer
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@@ -178,4 +178,4 @@ pub struct EventQueryDto {
pub struct PaginationDto { pub struct PaginationDto {
pub limit: Option<i64>, pub limit: Option<i64>,
pub offset: Option<i64>, pub offset: Option<i64>,
} }
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::domain::entities::contact::{Address, Contact, ContactGroup, Email, Phone};
use chrono::{DateTime, NaiveDate, Utc}; use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::domain::entities::contact::{Contact, Email, Phone, Address, ContactGroup};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailDto { pub struct EmailDto {
@@ -221,4 +221,4 @@ pub struct UpdateContactGroupDto {
pub struct GroupMembershipDto { pub struct GroupMembershipDto {
pub group_id: String, pub group_id: String,
pub contact_id: String, pub contact_id: String,
} }
+6 -6
View File
@@ -1,21 +1,21 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// DTO for favorites item /// DTO for favorites item
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FavoriteItemDto { pub struct FavoriteItemDto {
/// Unique identifier for the favorite entry /// Unique identifier for the favorite entry
pub id: String, pub id: String,
/// User ID who owns this favorite /// User ID who owns this favorite
pub user_id: String, pub user_id: String,
/// ID of the favorited item (file or folder) /// ID of the favorited item (file or folder)
pub item_id: String, pub item_id: String,
/// Type of the item ('file' or 'folder') /// Type of the item ('file' or 'folder')
pub item_type: String, pub item_type: String,
/// When the item was added to favorites /// When the item was added to favorites
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
} }
+12 -12
View File
@@ -1,30 +1,30 @@
use serde::{Serialize, Deserialize};
use crate::domain::entities::file::File; use crate::domain::entities::file::File;
use serde::{Deserialize, Serialize};
/// DTO for file responses /// DTO for file responses
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileDto { pub struct FileDto {
/// File ID /// File ID
pub id: String, pub id: String,
/// File name /// File name
pub name: String, pub name: String,
/// Path to the file (relative) /// Path to the file (relative)
pub path: String, pub path: String,
/// Size in bytes /// Size in bytes
pub size: u64, pub size: u64,
/// MIME type /// MIME type
pub mime_type: String, pub mime_type: String,
/// Parent folder ID /// Parent folder ID
pub folder_id: Option<String>, pub folder_id: Option<String>,
/// Creation timestamp /// Creation timestamp
pub created_at: u64, pub created_at: u64,
/// Last modification timestamp /// Last modification timestamp
pub modified_at: u64, pub modified_at: u64,
} }
@@ -51,14 +51,14 @@ impl From<FileDto> for File {
// Note: this should be simplified if File has a proper constructor // Note: this should be simplified if File has a proper constructor
// If not, make the conversion as best as possible // If not, make the conversion as best as possible
File::from_dto( File::from_dto(
dto.id, dto.id,
dto.name, dto.name,
dto.path, dto.path,
dto.size, dto.size,
dto.mime_type, dto.mime_type,
dto.folder_id, dto.folder_id,
dto.created_at, dto.created_at,
dto.modified_at dto.modified_at,
) )
} }
} }
@@ -83,4 +83,4 @@ impl Default for FileDto {
fn default() -> Self { fn default() -> Self {
Self::empty() Self::empty()
} }
} }
+11 -11
View File
@@ -1,12 +1,12 @@
use serde::{Serialize, Deserialize};
use crate::domain::entities::folder::Folder; use crate::domain::entities::folder::Folder;
use serde::{Deserialize, Serialize};
/// DTO for folder creation requests /// DTO for folder creation requests
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct CreateFolderDto { pub struct CreateFolderDto {
/// Name of the folder to create /// Name of the folder to create
pub name: String, pub name: String,
/// Parent folder ID (None for root level) /// Parent folder ID (None for root level)
pub parent_id: Option<String>, pub parent_id: Option<String>,
} }
@@ -30,22 +30,22 @@ pub struct MoveFolderDto {
pub struct FolderDto { pub struct FolderDto {
/// Folder ID /// Folder ID
pub id: String, pub id: String,
/// Folder name /// Folder name
pub name: String, pub name: String,
/// Path to the folder (relative) /// Path to the folder (relative)
pub path: String, pub path: String,
/// Parent folder ID /// Parent folder ID
pub parent_id: Option<String>, pub parent_id: Option<String>,
/// Creation timestamp /// Creation timestamp
pub created_at: u64, pub created_at: u64,
/// Last modification timestamp /// Last modification timestamp
pub modified_at: u64, pub modified_at: u64,
/// Whether this is a root folder /// Whether this is a root folder
pub is_root: bool, pub is_root: bool,
} }
@@ -53,7 +53,7 @@ pub struct FolderDto {
impl From<Folder> for FolderDto { impl From<Folder> for FolderDto {
fn from(folder: Folder) -> Self { fn from(folder: Folder) -> Self {
let is_root = folder.parent_id().is_none(); let is_root = folder.parent_id().is_none();
Self { Self {
id: folder.id().to_string(), id: folder.id().to_string(),
name: folder.name().to_string(), name: folder.name().to_string(),
@@ -77,7 +77,7 @@ impl From<FolderDto> for Folder {
dto.path, dto.path,
dto.parent_id, dto.parent_id,
dto.created_at, dto.created_at,
dto.modified_at dto.modified_at,
) )
} }
} }
@@ -101,4 +101,4 @@ impl Default for FolderDto {
fn default() -> Self { fn default() -> Self {
Self::empty() Self::empty()
} }
} }
+9 -9
View File
@@ -1,12 +1,12 @@
use serde::{Serialize, Deserialize};
use crate::domain::services::i18n_service::Locale; use crate::domain::services::i18n_service::Locale;
use serde::{Deserialize, Serialize};
/// DTO for locale information /// DTO for locale information
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct LocaleDto { pub struct LocaleDto {
/// Locale code (e.g., "en", "es") /// Locale code (e.g., "en", "es")
pub code: String, pub code: String,
/// Locale name in its own language (e.g., "English", "Español") /// Locale name in its own language (e.g., "English", "Español")
pub name: String, pub name: String,
} }
@@ -20,7 +20,7 @@ impl From<Locale> for LocaleDto {
Locale::German => ("de", "Deutsch"), Locale::German => ("de", "Deutsch"),
Locale::Portuguese => ("pt", "Português"), Locale::Portuguese => ("pt", "Português"),
}; };
Self { Self {
code: code.to_string(), code: code.to_string(),
name: name.to_string(), name: name.to_string(),
@@ -33,7 +33,7 @@ impl From<Locale> for LocaleDto {
pub struct TranslationRequestDto { pub struct TranslationRequestDto {
/// The translation key /// The translation key
pub key: String, pub key: String,
/// The locale code (optional, defaults to "en") /// The locale code (optional, defaults to "en")
pub locale: Option<String>, pub locale: Option<String>,
} }
@@ -43,10 +43,10 @@ pub struct TranslationRequestDto {
pub struct TranslationResponseDto { pub struct TranslationResponseDto {
/// The translation key /// The translation key
pub key: String, pub key: String,
/// The locale code used for translation /// The locale code used for translation
pub locale: String, pub locale: String,
/// The translated text /// The translated text
pub text: String, pub text: String,
} }
@@ -56,10 +56,10 @@ pub struct TranslationResponseDto {
pub struct TranslationErrorDto { pub struct TranslationErrorDto {
/// The translation key that was not found /// The translation key that was not found
pub key: String, pub key: String,
/// The locale code used for translation /// The locale code used for translation
pub locale: String, pub locale: String,
/// The error message /// The error message
pub error: String, pub error: String,
} }
-1
View File
@@ -12,4 +12,3 @@ pub mod settings_dto;
pub mod share_dto; pub mod share_dto;
pub mod trash_dto; pub mod trash_dto;
pub mod user_dto; pub mod user_dto;
+12 -23
View File
@@ -1,4 +1,4 @@
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
/// A DTO to represent pagination information /// A DTO to represent pagination information
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -56,50 +56,42 @@ impl PaginationRequestDto {
pub fn offset(&self) -> usize { pub fn offset(&self) -> usize {
self.page * self.page_size self.page * self.page_size
} }
/// Calculates the limit for paginated queries /// Calculates the limit for paginated queries
pub fn limit(&self) -> usize { pub fn limit(&self) -> usize {
self.page_size self.page_size
} }
/// Validates and adjusts the pagination parameters /// Validates and adjusts the pagination parameters
pub fn validate_and_adjust(&self) -> Self { pub fn validate_and_adjust(&self) -> Self {
let mut page = self.page; let mut page = self.page;
let mut page_size = self.page_size; let mut page_size = self.page_size;
// Ensure the page is at least 0 // Ensure the page is at least 0
if page < 1 { if page < 1 {
page = 0; page = 0;
} }
// Ensure the page size is between 10 and 500 // Ensure the page size is between 10 and 500
if page_size < 10 { if page_size < 10 {
page_size = 10; page_size = 10;
} else if page_size > 500 { } else if page_size > 500 {
page_size = 500; page_size = 500;
} }
Self { Self { page, page_size }
page,
page_size,
}
} }
} }
impl<T> PaginatedResponseDto<T> { impl<T> PaginatedResponseDto<T> {
/// Creates a new paginated response from the data and pagination information /// Creates a new paginated response from the data and pagination information
pub fn new( pub fn new(items: Vec<T>, page: usize, page_size: usize, total_items: usize) -> Self {
items: Vec<T>,
page: usize,
page_size: usize,
total_items: usize,
) -> Self {
let total_pages = if total_items == 0 { let total_pages = if total_items == 0 {
0 0
} else { } else {
total_items.div_ceil(page_size) total_items.div_ceil(page_size)
}; };
let pagination = PaginationDto { let pagination = PaginationDto {
page, page,
page_size, page_size,
@@ -108,10 +100,7 @@ impl<T> PaginatedResponseDto<T> {
has_next: page < total_pages - 1, has_next: page < total_pages - 1,
has_prev: page > 0, has_prev: page > 0,
}; };
Self { Self { items, pagination }
items,
pagination,
}
} }
} }
+6 -6
View File
@@ -1,21 +1,21 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// DTO for recent items /// DTO for recent items
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecentItemDto { pub struct RecentItemDto {
/// Unique identifier for the recent item /// Unique identifier for the recent item
pub id: String, pub id: String,
/// Owner user ID /// Owner user ID
pub user_id: String, pub user_id: String,
/// Item ID (file or folder) /// Item ID (file or folder)
pub item_id: String, pub item_id: String,
/// Item type ('file' or 'folder') /// Item type ('file' or 'folder')
pub item_type: String, pub item_type: String,
/// When the item was accessed /// When the item was accessed
pub accessed_at: DateTime<Utc>, pub accessed_at: DateTime<Utc>,
} }
+23 -23
View File
@@ -1,9 +1,9 @@
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
/** /**
* Data Transfer Object for file search criteria. * Data Transfer Object for file search criteria.
* *
* This structure represents all possible search parameters that can be used * This structure represents all possible search parameters that can be used
* to filter files and folders in the system. It supports various filter types * to filter files and folders in the system. It supports various filter types
* including name matching, file types, date ranges, and size constraints. * including name matching, file types, date ranges, and size constraints.
*/ */
@@ -12,47 +12,47 @@ pub struct SearchCriteriaDto {
/// Optional text to search in file/folder names /// Optional text to search in file/folder names
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub name_contains: Option<String>, pub name_contains: Option<String>,
/// Optional list of file extensions to include (e.g., "pdf", "jpg") /// Optional list of file extensions to include (e.g., "pdf", "jpg")
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub file_types: Option<Vec<String>>, pub file_types: Option<Vec<String>>,
/// Optional minimum creation date (seconds since epoch) /// Optional minimum creation date (seconds since epoch)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub created_after: Option<u64>, pub created_after: Option<u64>,
/// Optional maximum creation date (seconds since epoch) /// Optional maximum creation date (seconds since epoch)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub created_before: Option<u64>, pub created_before: Option<u64>,
/// Optional minimum modification date (seconds since epoch) /// Optional minimum modification date (seconds since epoch)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub modified_after: Option<u64>, pub modified_after: Option<u64>,
/// Optional maximum modification date (seconds since epoch) /// Optional maximum modification date (seconds since epoch)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub modified_before: Option<u64>, pub modified_before: Option<u64>,
/// Optional minimum file size in bytes /// Optional minimum file size in bytes
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub min_size: Option<u64>, pub min_size: Option<u64>,
/// Optional maximum file size in bytes /// Optional maximum file size in bytes
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub max_size: Option<u64>, pub max_size: Option<u64>,
/// Optional folder ID to limit search scope /// Optional folder ID to limit search scope
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub folder_id: Option<String>, pub folder_id: Option<String>,
/// Whether to search recursively within subfolders (default: true) /// Whether to search recursively within subfolders (default: true)
#[serde(default = "default_recursive")] #[serde(default = "default_recursive")]
pub recursive: bool, pub recursive: bool,
/// Maximum number of results to return /// Maximum number of results to return
#[serde(default = "default_limit")] #[serde(default = "default_limit")]
pub limit: usize, pub limit: usize,
/// Offset for pagination /// Offset for pagination
#[serde(default)] #[serde(default)]
pub offset: usize, pub offset: usize,
@@ -89,7 +89,7 @@ impl Default for SearchCriteriaDto {
/** /**
* Data Transfer Object for search results. * Data Transfer Object for search results.
* *
* This structure encapsulates the results of a search operation, including * This structure encapsulates the results of a search operation, including
* both files and folders that match the search criteria, along with pagination information. * both files and folders that match the search criteria, along with pagination information.
*/ */
@@ -97,19 +97,19 @@ impl Default for SearchCriteriaDto {
pub struct SearchResultsDto { pub struct SearchResultsDto {
/// Files matching the search criteria /// Files matching the search criteria
pub files: Vec<crate::application::dtos::file_dto::FileDto>, pub files: Vec<crate::application::dtos::file_dto::FileDto>,
/// Folders matching the search criteria /// Folders matching the search criteria
pub folders: Vec<crate::application::dtos::folder_dto::FolderDto>, pub folders: Vec<crate::application::dtos::folder_dto::FolderDto>,
/// Total count of matching items (for pagination) /// Total count of matching items (for pagination)
pub total_count: Option<usize>, pub total_count: Option<usize>,
/// Limit used in the search /// Limit used in the search
pub limit: usize, pub limit: usize,
/// Offset used in the search /// Offset used in the search
pub offset: usize, pub offset: usize,
/// Whether there are more results available /// Whether there are more results available
pub has_more: bool, pub has_more: bool,
} }
@@ -126,7 +126,7 @@ impl SearchResultsDto {
has_more: false, has_more: false,
} }
} }
/// Creates a new search results object from files and folders /// Creates a new search results object from files and folders
pub fn new( pub fn new(
files: Vec<crate::application::dtos::file_dto::FileDto>, files: Vec<crate::application::dtos::file_dto::FileDto>,
@@ -139,7 +139,7 @@ impl SearchResultsDto {
Some(total) => (offset + files.len() + folders.len()) < total, Some(total) => (offset + files.len() + folders.len()) < total,
None => false, None => false,
}; };
Self { Self {
files, files,
folders, folders,
@@ -149,4 +149,4 @@ impl SearchResultsDto {
has_more, has_more,
} }
} }
} }
+130 -130
View File
@@ -1,130 +1,130 @@
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
// ============================================================================ // ============================================================================
// OIDC Settings DTOs (Admin Panel) // OIDC Settings DTOs (Admin Panel)
// ============================================================================ // ============================================================================
/// Current OIDC settings returned to admin UI (secrets masked) /// Current OIDC settings returned to admin UI (secrets masked)
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct OidcSettingsDto { pub struct OidcSettingsDto {
pub enabled: bool, pub enabled: bool,
pub issuer_url: String, pub issuer_url: String,
pub client_id: String, pub client_id: String,
/// True if a client secret is configured (never reveals the actual value) /// True if a client secret is configured (never reveals the actual value)
pub client_secret_set: bool, pub client_secret_set: bool,
pub scopes: String, pub scopes: String,
pub auto_provision: bool, pub auto_provision: bool,
pub admin_groups: String, pub admin_groups: String,
pub disable_password_login: bool, pub disable_password_login: bool,
pub provider_name: String, pub provider_name: String,
/// Auto-generated callback URL the admin must register in their IdP /// Auto-generated callback URL the admin must register in their IdP
pub callback_url: String, pub callback_url: String,
/// Field names overridden by environment variables (read-only in UI) /// Field names overridden by environment variables (read-only in UI)
pub env_overrides: Vec<String>, pub env_overrides: Vec<String>,
} }
/// Request body for saving OIDC settings from the admin panel /// Request body for saving OIDC settings from the admin panel
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct SaveOidcSettingsDto { pub struct SaveOidcSettingsDto {
pub enabled: bool, pub enabled: bool,
pub issuer_url: String, pub issuer_url: String,
pub client_id: String, pub client_id: String,
/// Only update if provided and non-empty (None = keep existing) /// Only update if provided and non-empty (None = keep existing)
pub client_secret: Option<String>, pub client_secret: Option<String>,
pub scopes: Option<String>, pub scopes: Option<String>,
pub auto_provision: Option<bool>, pub auto_provision: Option<bool>,
pub admin_groups: Option<String>, pub admin_groups: Option<String>,
pub disable_password_login: Option<bool>, pub disable_password_login: Option<bool>,
pub provider_name: Option<String>, pub provider_name: Option<String>,
} }
/// Request body for testing OIDC discovery /// Request body for testing OIDC discovery
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct TestOidcConnectionDto { pub struct TestOidcConnectionDto {
pub issuer_url: String, pub issuer_url: String,
} }
/// Result of OIDC connection test /// Result of OIDC connection test
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct OidcTestResultDto { pub struct OidcTestResultDto {
pub success: bool, pub success: bool,
pub message: String, pub message: String,
pub issuer: Option<String>, pub issuer: Option<String>,
pub authorization_endpoint: Option<String>, pub authorization_endpoint: Option<String>,
pub token_endpoint: Option<String>, pub token_endpoint: Option<String>,
pub userinfo_endpoint: Option<String>, pub userinfo_endpoint: Option<String>,
/// Suggested provider name (derived from issuer hostname) /// Suggested provider name (derived from issuer hostname)
pub provider_name_suggestion: Option<String>, pub provider_name_suggestion: Option<String>,
} }
// ============================================================================ // ============================================================================
// Admin User Management DTOs // Admin User Management DTOs
// ============================================================================ // ============================================================================
/// Request body for updating a user's role /// Request body for updating a user's role
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct UpdateUserRoleDto { pub struct UpdateUserRoleDto {
pub role: String, pub role: String,
} }
/// Request body for updating a user's active status /// Request body for updating a user's active status
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct UpdateUserActiveDto { pub struct UpdateUserActiveDto {
pub active: bool, pub active: bool,
} }
/// Request body for updating a user's storage quota /// Request body for updating a user's storage quota
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct UpdateUserQuotaDto { pub struct UpdateUserQuotaDto {
/// Quota in bytes. Use 0 for unlimited. /// Quota in bytes. Use 0 for unlimited.
pub quota_bytes: i64, pub quota_bytes: i64,
} }
/// Request body for admin-created users /// Request body for admin-created users
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AdminCreateUserDto { pub struct AdminCreateUserDto {
pub username: String, pub username: String,
pub password: String, pub password: String,
/// Optional — if omitted, a placeholder email is generated /// Optional — if omitted, a placeholder email is generated
pub email: Option<String>, pub email: Option<String>,
/// "admin" or "user"; defaults to "user" /// "admin" or "user"; defaults to "user"
pub role: Option<String>, pub role: Option<String>,
/// Storage quota in bytes; 0 = unlimited. If omitted, uses role default. /// Storage quota in bytes; 0 = unlimited. If omitted, uses role default.
pub quota_bytes: Option<i64>, pub quota_bytes: Option<i64>,
/// Whether the account is active; defaults to true /// Whether the account is active; defaults to true
pub active: Option<bool>, pub active: Option<bool>,
} }
/// Request body for admin password reset /// Request body for admin password reset
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct AdminResetPasswordDto { pub struct AdminResetPasswordDto {
pub new_password: String, pub new_password: String,
} }
/// Query parameters for listing users /// Query parameters for listing users
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ListUsersQueryDto { pub struct ListUsersQueryDto {
pub limit: Option<i64>, pub limit: Option<i64>,
pub offset: Option<i64>, pub offset: Option<i64>,
} }
/// Dashboard statistics /// Dashboard statistics
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct DashboardStatsDto { pub struct DashboardStatsDto {
// System info // System info
pub server_version: String, pub server_version: String,
pub auth_enabled: bool, pub auth_enabled: bool,
pub oidc_configured: bool, pub oidc_configured: bool,
pub quotas_enabled: bool, pub quotas_enabled: bool,
// User stats // User stats
pub total_users: i64, pub total_users: i64,
pub active_users: i64, pub active_users: i64,
pub admin_users: i64, pub admin_users: i64,
// Storage stats // Storage stats
pub total_quota_bytes: i64, pub total_quota_bytes: i64,
pub total_used_bytes: i64, pub total_used_bytes: i64,
pub storage_usage_percent: f64, pub storage_usage_percent: f64,
pub users_over_80_percent: i64, pub users_over_80_percent: i64,
pub users_over_quota: i64, pub users_over_quota: i64,
pub registration_enabled: bool, pub registration_enabled: bool,
} }
+2 -2
View File
@@ -44,7 +44,7 @@ pub struct UpdateShareDto {
impl ShareDto { impl ShareDto {
pub fn from_entity(share: &Share, base_url: &str) -> Self { pub fn from_entity(share: &Share, base_url: &str) -> Self {
let url = format!("{}/s/{}", base_url, share.token()); let url = format!("{}/s/{}", base_url, share.token());
Self { Self {
id: share.id().to_string(), id: share.id().to_string(),
item_id: share.item_id().to_string(), item_id: share.item_id().to_string(),
@@ -69,7 +69,7 @@ impl SharePermissionsDto {
reshare: permissions.reshare(), reshare: permissions.reshare(),
} }
} }
pub fn to_entity(&self) -> SharePermissions { pub fn to_entity(&self) -> SharePermissions {
SharePermissions::new(self.read, self.write, self.reshare) SharePermissions::new(self.read, self.write, self.reshare)
} }
+1 -1
View File
@@ -30,4 +30,4 @@ pub struct RestoreFromTrashRequest {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct DeletePermanentlyRequest { pub struct DeletePermanentlyRequest {
pub trash_id: String, pub trash_id: String,
} }
+3 -3
View File
@@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
use crate::domain::entities::user::User; use crate::domain::entities::user::User;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct UserDto { pub struct UserDto {
@@ -117,4 +117,4 @@ pub struct OidcUserInfoDto {
pub email: Option<String>, pub email: Option<String>,
pub name: Option<String>, pub name: Option<String>,
pub groups: Vec<String>, pub groups: Vec<String>,
} }
+2 -2
View File
@@ -1,7 +1,7 @@
pub mod adapters;
pub mod dtos; pub mod dtos;
pub mod ports; pub mod ports;
pub mod services; pub mod services;
pub mod transactions; pub mod transactions;
pub mod adapters;
// Re-exportaciones para facilitar el acceso a los principales puertos // Re-exportaciones para facilitar el acceso a los principales puertos
+59 -31
View File
@@ -1,20 +1,20 @@
use async_trait::async_trait;
use crate::domain::entities::user::User;
use crate::domain::entities::session::Session;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use crate::domain::entities::session::Session;
use crate::domain::entities::user::User;
use async_trait::async_trait;
// ============================================================================ // ============================================================================
// Cryptography Ports - Extracted from Domain to maintain Clean Architecture // Cryptography Ports - Extracted from Domain to maintain Clean Architecture
// ============================================================================ // ============================================================================
/// Port for password hashing operations. /// Port for password hashing operations.
/// ///
/// This trait abstracts cryptographic password operations, allowing the domain /// This trait abstracts cryptographic password operations, allowing the domain
/// layer to remain independent of specific hashing implementations (argon2, bcrypt, etc.) /// layer to remain independent of specific hashing implementations (argon2, bcrypt, etc.)
pub trait PasswordHasherPort: Send + Sync + 'static { pub trait PasswordHasherPort: Send + Sync + 'static {
/// Hash a plain text password /// Hash a plain text password
fn hash_password(&self, password: &str) -> Result<String, DomainError>; fn hash_password(&self, password: &str) -> Result<String, DomainError>;
/// Verify a plain text password against a hash /// Verify a plain text password against a hash
fn verify_password(&self, password: &str, hash: &str) -> Result<bool, DomainError>; fn verify_password(&self, password: &str, hash: &str) -> Result<bool, DomainError>;
} }
@@ -39,22 +39,22 @@ pub struct TokenClaims {
} }
/// Port for JWT token operations. /// Port for JWT token operations.
/// ///
/// This trait abstracts token generation and validation, allowing the domain /// This trait abstracts token generation and validation, allowing the domain
/// layer to remain independent of specific JWT implementations. /// layer to remain independent of specific JWT implementations.
pub trait TokenServicePort: Send + Sync + 'static { pub trait TokenServicePort: Send + Sync + 'static {
/// Generate an access token for a user /// Generate an access token for a user
fn generate_access_token(&self, user: &User) -> Result<String, DomainError>; fn generate_access_token(&self, user: &User) -> Result<String, DomainError>;
/// Validate a token and extract its claims /// Validate a token and extract its claims
fn validate_token(&self, token: &str) -> Result<TokenClaims, DomainError>; fn validate_token(&self, token: &str) -> Result<TokenClaims, DomainError>;
/// Generate a refresh token /// Generate a refresh token
fn generate_refresh_token(&self) -> String; fn generate_refresh_token(&self) -> String;
/// Get refresh token expiry in seconds /// Get refresh token expiry in seconds
fn refresh_token_expiry_secs(&self) -> i64; fn refresh_token_expiry_secs(&self) -> i64;
/// Get refresh token expiry in days /// Get refresh token expiry in days
fn refresh_token_expiry_days(&self) -> i64; fn refresh_token_expiry_days(&self) -> i64;
} }
@@ -65,38 +65,46 @@ pub trait TokenServicePort: Send + Sync + 'static {
#[async_trait] #[async_trait]
pub trait UserStoragePort: Send + Sync + 'static { pub trait UserStoragePort: Send + Sync + 'static {
/// Creates a new user /// Creates a new user
async fn create_user(&self, user: User) -> Result<User, DomainError>; async fn create_user(&self, user: User) -> Result<User, DomainError>;
/// Gets a user by ID /// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError>; async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError>;
/// Gets a user by username /// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError>; async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError>;
/// Gets a user by email /// Gets a user by email
async fn get_user_by_email(&self, email: &str) -> Result<User, DomainError>; async fn get_user_by_email(&self, email: &str) -> Result<User, DomainError>;
/// Updates an existing user /// Updates an existing user
async fn update_user(&self, user: User) -> Result<User, DomainError>; async fn update_user(&self, user: User) -> Result<User, DomainError>;
/// Updates only the storage usage of a user /// Updates only the storage usage of a user
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> Result<(), DomainError>; async fn update_storage_usage(
&self,
user_id: &str,
usage_bytes: i64,
) -> Result<(), DomainError>;
/// Lists users with pagination /// Lists users with pagination
async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<User>, DomainError>; async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<User>, DomainError>;
/// Lists users by role (e.g., "admin" or "user") /// Lists users by role (e.g., "admin" or "user")
async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError>; async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError>;
/// Deletes a user by their ID /// Deletes a user by their ID
async fn delete_user(&self, user_id: &str) -> Result<(), DomainError>; async fn delete_user(&self, user_id: &str) -> Result<(), DomainError>;
/// Changes a user's password /// Changes a user's password
async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>; async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>;
/// Finds a user by OIDC provider + subject pair /// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> Result<User, DomainError>; async fn get_user_by_oidc_subject(
&self,
provider: &str,
subject: &str,
) -> Result<User, DomainError>;
/// Activates or deactivates a user /// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError>; async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError>;
@@ -105,7 +113,11 @@ pub trait UserStoragePort: Send + Sync + 'static {
async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError>; async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError>;
/// Updates a user's storage quota /// Updates a user's storage quota
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> Result<(), DomainError>; async fn update_storage_quota(
&self,
user_id: &str,
quota_bytes: i64,
) -> Result<(), DomainError>;
/// Counts the total number of users /// Counts the total number of users
async fn count_users(&self) -> Result<i64, DomainError>; async fn count_users(&self) -> Result<i64, DomainError>;
@@ -139,14 +151,27 @@ pub trait OidcServicePort: Send + Sync + 'static {
/// Get the authorization URL for redirecting the user to the IdP. /// Get the authorization URL for redirecting the user to the IdP.
/// Includes PKCE code_challenge (S256) and nonce for ID token binding. /// Includes PKCE code_challenge (S256) and nonce for ID token binding.
/// This is async because it may need to fetch the OIDC discovery document. /// This is async because it may need to fetch the OIDC discovery document.
async fn get_authorize_url(&self, state: &str, nonce: &str, pkce_challenge: &str) -> Result<String, DomainError>; async fn get_authorize_url(
&self,
state: &str,
nonce: &str,
pkce_challenge: &str,
) -> Result<String, DomainError>;
/// Exchange an authorization code for tokens, providing PKCE code_verifier. /// Exchange an authorization code for tokens, providing PKCE code_verifier.
async fn exchange_code(&self, code: &str, pkce_verifier: &str) -> Result<OidcTokenSet, DomainError>; async fn exchange_code(
&self,
code: &str,
pkce_verifier: &str,
) -> Result<OidcTokenSet, DomainError>;
/// Validate an ID token and extract claims. /// Validate an ID token and extract claims.
/// If `expected_nonce` is provided, verifies the `nonce` claim matches. /// If `expected_nonce` is provided, verifies the `nonce` claim matches.
async fn validate_id_token(&self, id_token: &str, expected_nonce: Option<&str>) -> Result<OidcIdClaims, DomainError>; async fn validate_id_token(
&self,
id_token: &str,
expected_nonce: Option<&str>,
) -> Result<OidcIdClaims, DomainError>;
/// Fetch user info from the UserInfo endpoint (fallback for missing ID token claims) /// Fetch user info from the UserInfo endpoint (fallback for missing ID token claims)
async fn fetch_user_info(&self, access_token: &str) -> Result<OidcIdClaims, DomainError>; async fn fetch_user_info(&self, access_token: &str) -> Result<OidcIdClaims, DomainError>;
@@ -159,13 +184,16 @@ pub trait OidcServicePort: Send + Sync + 'static {
pub trait SessionStoragePort: Send + Sync + 'static { pub trait SessionStoragePort: Send + Sync + 'static {
/// Creates a new session /// Creates a new session
async fn create_session(&self, session: Session) -> Result<Session, DomainError>; async fn create_session(&self, session: Session) -> Result<Session, DomainError>;
/// Gets a session by refresh token /// Gets a session by refresh token
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> Result<Session, DomainError>; async fn get_session_by_refresh_token(
&self,
refresh_token: &str,
) -> Result<Session, DomainError>;
/// Revokes a specific session /// Revokes a specific session
async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>; async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>;
/// Revokes all sessions of a user /// Revokes all sessions of a user
async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError>; async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError>;
} }
+2 -2
View File
@@ -8,10 +8,10 @@
//! The application and interface layers remain independent of the caching //! The application and interface layers remain independent of the caching
//! implementation details. //! implementation details.
use std::path::{Path, PathBuf}; use crate::common::errors::DomainError;
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use crate::common::errors::DomainError; use std::path::{Path, PathBuf};
/// Statistics for monitoring write-behind cache status. /// Statistics for monitoring write-behind cache status.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
+187 -54
View File
@@ -1,47 +1,108 @@
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use crate::application::dtos::calendar_dto::{ use crate::application::dtos::calendar_dto::{
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto, CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto,
CreateEventDto, UpdateEventDto, CreateEventICalDto UpdateCalendarDto, UpdateEventDto,
}; };
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
/// Port for external calendar storage mechanisms /// Port for external calendar storage mechanisms
#[async_trait] #[async_trait]
pub trait CalendarStoragePort: Send + Sync + 'static { pub trait CalendarStoragePort: Send + Sync + 'static {
// Calendar operations // Calendar operations
async fn create_calendar(&self, calendar: CreateCalendarDto, owner_id: &str) -> Result<CalendarDto, DomainError>; async fn create_calendar(
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError>; &self,
calendar: CreateCalendarDto,
owner_id: &str,
) -> Result<CalendarDto, DomainError>;
async fn update_calendar(
&self,
calendar_id: &str,
update: UpdateCalendarDto,
) -> Result<CalendarDto, DomainError>;
async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError>; async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError>;
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError>; async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError>;
async fn list_calendars_by_owner(&self, owner_id: &str) -> Result<Vec<CalendarDto>, DomainError>; async fn list_calendars_by_owner(
async fn list_calendars_shared_with_user(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError>; &self,
async fn list_public_calendars(&self, limit: i64, offset: i64) -> Result<Vec<CalendarDto>, DomainError>; owner_id: &str,
async fn check_calendar_access(&self, calendar_id: &str, user_id: &str) -> Result<bool, DomainError>; ) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_calendars_shared_with_user(
&self,
user_id: &str,
) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_public_calendars(
&self,
limit: i64,
offset: i64,
) -> Result<Vec<CalendarDto>, DomainError>;
async fn check_calendar_access(
&self,
calendar_id: &str,
user_id: &str,
) -> Result<bool, DomainError>;
// Calendar sharing // Calendar sharing
async fn share_calendar(&self, calendar_id: &str, user_id: &str, access_level: &str) -> Result<(), DomainError>; async fn share_calendar(
async fn remove_calendar_sharing(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError>; &self,
async fn get_calendar_shares(&self, calendar_id: &str) -> Result<Vec<(String, String)>, DomainError>; calendar_id: &str,
user_id: &str,
access_level: &str,
) -> Result<(), DomainError>;
async fn remove_calendar_sharing(
&self,
calendar_id: &str,
user_id: &str,
) -> Result<(), DomainError>;
async fn get_calendar_shares(
&self,
calendar_id: &str,
) -> Result<Vec<(String, String)>, DomainError>;
// Calendar properties // Calendar properties
async fn set_calendar_property(&self, calendar_id: &str, property_name: &str, property_value: &str) -> Result<(), DomainError>; async fn set_calendar_property(
async fn get_calendar_property(&self, calendar_id: &str, property_name: &str) -> Result<Option<String>, DomainError>; &self,
async fn get_calendar_properties(&self, calendar_id: &str) -> Result<std::collections::HashMap<String, String>, DomainError>; calendar_id: &str,
property_name: &str,
property_value: &str,
) -> Result<(), DomainError>;
async fn get_calendar_property(
&self,
calendar_id: &str,
property_name: &str,
) -> Result<Option<String>, DomainError>;
async fn get_calendar_properties(
&self,
calendar_id: &str,
) -> Result<std::collections::HashMap<String, String>, DomainError>;
// Event operations // Event operations
async fn create_event(&self, event: CreateEventDto) -> Result<CalendarEventDto, DomainError>; async fn create_event(&self, event: CreateEventDto) -> Result<CalendarEventDto, DomainError>;
async fn create_event_from_ical(&self, event: CreateEventICalDto) -> Result<CalendarEventDto, DomainError>; async fn create_event_from_ical(
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError>; &self,
event: CreateEventICalDto,
) -> Result<CalendarEventDto, DomainError>;
async fn update_event(
&self,
event_id: &str,
update: UpdateEventDto,
) -> Result<CalendarEventDto, DomainError>;
async fn delete_event(&self, event_id: &str) -> Result<(), DomainError>; async fn delete_event(&self, event_id: &str) -> Result<(), DomainError>;
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError>; async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError>;
async fn list_events_by_calendar(&self, calendar_id: &str) -> Result<Vec<CalendarEventDto>, DomainError>; async fn list_events_by_calendar(
async fn list_events_by_calendar_paginated(&self, calendar_id: &str, limit: i64, offset: i64) -> Result<Vec<CalendarEventDto>, DomainError>; &self,
calendar_id: &str,
) -> Result<Vec<CalendarEventDto>, DomainError>;
async fn list_events_by_calendar_paginated(
&self,
calendar_id: &str,
limit: i64,
offset: i64,
) -> Result<Vec<CalendarEventDto>, DomainError>;
async fn get_events_in_time_range( async fn get_events_in_time_range(
&self, &self,
calendar_id: &str, calendar_id: &str,
start: &DateTime<Utc>, start: &DateTime<Utc>,
end: &DateTime<Utc> end: &DateTime<Utc>,
) -> Result<Vec<CalendarEventDto>, DomainError>; ) -> Result<Vec<CalendarEventDto>, DomainError>;
} }
@@ -49,41 +110,113 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
#[async_trait] #[async_trait]
pub trait CalendarUseCase: Send + Sync + 'static { pub trait CalendarUseCase: Send + Sync + 'static {
// Calendar operations // Calendar operations
async fn create_calendar(&self, calendar: CreateCalendarDto) -> Result<CalendarDto, DomainError>; async fn create_calendar(
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError>; &self,
calendar: CreateCalendarDto,
) -> Result<CalendarDto, DomainError>;
async fn update_calendar(
&self,
calendar_id: &str,
update: UpdateCalendarDto,
) -> Result<CalendarDto, DomainError>;
async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError>; async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError>;
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError>; async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError>;
async fn list_my_calendars(&self) -> Result<Vec<CalendarDto>, DomainError>; async fn list_my_calendars(&self) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_shared_calendars(&self) -> Result<Vec<CalendarDto>, DomainError>; async fn list_shared_calendars(&self) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_public_calendars(&self, limit: Option<i64>, offset: Option<i64>) -> Result<Vec<CalendarDto>, DomainError>; async fn list_public_calendars(
&self,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<CalendarDto>, DomainError>;
// Calendar sharing // Calendar sharing
async fn share_calendar(&self, calendar_id: &str, user_id: &str, access_level: &str) -> Result<(), DomainError>; async fn share_calendar(
async fn remove_calendar_sharing(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError>; &self,
async fn get_calendar_shares(&self, calendar_id: &str) -> Result<Vec<(String, String)>, DomainError>; calendar_id: &str,
user_id: &str,
access_level: &str,
) -> Result<(), DomainError>;
async fn remove_calendar_sharing(
&self,
calendar_id: &str,
user_id: &str,
) -> Result<(), DomainError>;
async fn get_calendar_shares(
&self,
calendar_id: &str,
) -> Result<Vec<(String, String)>, DomainError>;
// Event operations // Event operations
async fn create_event(&self, event: CreateEventDto) -> Result<CalendarEventDto, DomainError>; async fn create_event(&self, event: CreateEventDto) -> Result<CalendarEventDto, DomainError>;
async fn create_event_from_ical(&self, event: CreateEventICalDto) -> Result<CalendarEventDto, DomainError>; async fn create_event_from_ical(
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError>; &self,
event: CreateEventICalDto,
) -> Result<CalendarEventDto, DomainError>;
async fn update_event(
&self,
event_id: &str,
update: UpdateEventDto,
) -> Result<CalendarEventDto, DomainError>;
async fn delete_event(&self, event_id: &str) -> Result<(), DomainError>; async fn delete_event(&self, event_id: &str) -> Result<(), DomainError>;
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError>; async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError>;
async fn list_events(&self, calendar_id: &str, limit: Option<i64>, offset: Option<i64>) -> Result<Vec<CalendarEventDto>, DomainError>; async fn list_events(
async fn get_events_in_range( &self,
&self, calendar_id: &str,
calendar_id: &str, limit: Option<i64>,
start: DateTime<Utc>, offset: Option<i64>,
end: DateTime<Utc>
) -> Result<Vec<CalendarEventDto>, DomainError>; ) -> Result<Vec<CalendarEventDto>, DomainError>;
async fn get_events_in_range(
&self,
calendar_id: &str,
start: DateTime<Utc>,
end: DateTime<Utc>,
) -> Result<Vec<CalendarEventDto>, DomainError>;
// ─── User-contextualized variants (for CalDAV protocol handler) ── // ─── User-contextualized variants (for CalDAV protocol handler) ──
async fn create_calendar_for_user(&self, calendar: CreateCalendarDto, user_id: &str) -> Result<CalendarDto, DomainError>; async fn create_calendar_for_user(
async fn update_calendar_for_user(&self, calendar_id: &str, update: UpdateCalendarDto, user_id: &str) -> Result<CalendarDto, DomainError>; &self,
async fn delete_calendar_for_user(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError>; calendar: CreateCalendarDto,
async fn get_calendar_for_user(&self, calendar_id: &str, user_id: &str) -> Result<CalendarDto, DomainError>; user_id: &str,
async fn list_my_calendars_for_user(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError>; ) -> Result<CalendarDto, DomainError>;
async fn list_events_for_user(&self, calendar_id: &str, limit: Option<i64>, offset: Option<i64>, user_id: &str) -> Result<Vec<CalendarEventDto>, DomainError>; async fn update_calendar_for_user(
async fn get_events_in_range_for_user(&self, calendar_id: &str, start: DateTime<Utc>, end: DateTime<Utc>, user_id: &str) -> Result<Vec<CalendarEventDto>, DomainError>; &self,
async fn create_event_from_ical_for_user(&self, event: CreateEventICalDto, user_id: &str) -> Result<CalendarEventDto, DomainError>; calendar_id: &str,
async fn delete_event_for_user(&self, event_id: &str, user_id: &str) -> Result<(), DomainError>; update: UpdateCalendarDto,
} user_id: &str,
) -> Result<CalendarDto, DomainError>;
async fn delete_calendar_for_user(
&self,
calendar_id: &str,
user_id: &str,
) -> Result<(), DomainError>;
async fn get_calendar_for_user(
&self,
calendar_id: &str,
user_id: &str,
) -> Result<CalendarDto, DomainError>;
async fn list_my_calendars_for_user(
&self,
user_id: &str,
) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_events_for_user(
&self,
calendar_id: &str,
limit: Option<i64>,
offset: Option<i64>,
user_id: &str,
) -> Result<Vec<CalendarEventDto>, DomainError>;
async fn get_events_in_range_for_user(
&self,
calendar_id: &str,
start: DateTime<Utc>,
end: DateTime<Utc>,
user_id: &str,
) -> Result<Vec<CalendarEventDto>, DomainError>;
async fn create_event_from_ical_for_user(
&self,
event: CreateEventICalDto,
user_id: &str,
) -> Result<CalendarEventDto, DomainError>;
async fn delete_event_for_user(&self, event_id: &str, user_id: &str)
-> Result<(), DomainError>;
}
+120 -34
View File
@@ -1,57 +1,143 @@
use async_trait::async_trait;
use crate::common::errors::DomainError;
use crate::application::dtos::address_book_dto::{ use crate::application::dtos::address_book_dto::{
AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto, AddressBookDto, CreateAddressBookDto, ShareAddressBookDto, UnshareAddressBookDto,
ShareAddressBookDto, UnshareAddressBookDto UpdateAddressBookDto,
}; };
use crate::application::dtos::contact_dto::{ use crate::application::dtos::contact_dto::{
ContactDto, CreateContactDto, UpdateContactDto, CreateContactVCardDto, ContactDto, ContactGroupDto, CreateContactDto, CreateContactGroupDto, CreateContactVCardDto,
ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto GroupMembershipDto, UpdateContactDto, UpdateContactGroupDto,
}; };
use crate::common::errors::DomainError;
use async_trait::async_trait;
pub type CardDavRepositoryError = DomainError; pub type CardDavRepositoryError = DomainError;
#[async_trait] #[async_trait]
pub trait AddressBookUseCase: Send + Sync + 'static { pub trait AddressBookUseCase: Send + Sync + 'static {
// Address Book operations // Address Book operations
async fn create_address_book(&self, dto: CreateAddressBookDto) -> Result<AddressBookDto, DomainError>; async fn create_address_book(
async fn update_address_book(&self, address_book_id: &str, update: UpdateAddressBookDto) -> Result<AddressBookDto, DomainError>; &self,
async fn delete_address_book(&self, address_book_id: &str, user_id: &str) -> Result<(), DomainError>; dto: CreateAddressBookDto,
async fn get_address_book(&self, address_book_id: &str, user_id: &str) -> Result<AddressBookDto, DomainError>; ) -> Result<AddressBookDto, DomainError>;
async fn list_user_address_books(&self, user_id: &str) -> Result<Vec<AddressBookDto>, DomainError>; async fn update_address_book(
&self,
address_book_id: &str,
update: UpdateAddressBookDto,
) -> Result<AddressBookDto, DomainError>;
async fn delete_address_book(
&self,
address_book_id: &str,
user_id: &str,
) -> Result<(), DomainError>;
async fn get_address_book(
&self,
address_book_id: &str,
user_id: &str,
) -> Result<AddressBookDto, DomainError>;
async fn list_user_address_books(
&self,
user_id: &str,
) -> Result<Vec<AddressBookDto>, DomainError>;
async fn list_public_address_books(&self) -> Result<Vec<AddressBookDto>, DomainError>; async fn list_public_address_books(&self) -> Result<Vec<AddressBookDto>, DomainError>;
// Address Book sharing // Address Book sharing
async fn share_address_book(&self, dto: ShareAddressBookDto, user_id: &str) -> Result<(), DomainError>; async fn share_address_book(
async fn unshare_address_book(&self, dto: UnshareAddressBookDto, user_id: &str) -> Result<(), DomainError>; &self,
async fn get_address_book_shares(&self, address_book_id: &str, user_id: &str) -> Result<Vec<(String, bool)>, DomainError>; dto: ShareAddressBookDto,
user_id: &str,
) -> Result<(), DomainError>;
async fn unshare_address_book(
&self,
dto: UnshareAddressBookDto,
user_id: &str,
) -> Result<(), DomainError>;
async fn get_address_book_shares(
&self,
address_book_id: &str,
user_id: &str,
) -> Result<Vec<(String, bool)>, DomainError>;
} }
#[async_trait] #[async_trait]
pub trait ContactUseCase: Send + Sync + 'static { pub trait ContactUseCase: Send + Sync + 'static {
// Contact operations // Contact operations
async fn create_contact(&self, dto: CreateContactDto) -> Result<ContactDto, DomainError>; async fn create_contact(&self, dto: CreateContactDto) -> Result<ContactDto, DomainError>;
async fn create_contact_from_vcard(&self, dto: CreateContactVCardDto) -> Result<ContactDto, DomainError>; async fn create_contact_from_vcard(
async fn update_contact(&self, contact_id: &str, update: UpdateContactDto) -> Result<ContactDto, DomainError>; &self,
dto: CreateContactVCardDto,
) -> Result<ContactDto, DomainError>;
async fn update_contact(
&self,
contact_id: &str,
update: UpdateContactDto,
) -> Result<ContactDto, DomainError>;
async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError>; async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn get_contact(&self, contact_id: &str, user_id: &str) -> Result<ContactDto, DomainError>; async fn get_contact(&self, contact_id: &str, user_id: &str)
async fn list_contacts(&self, address_book_id: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError>; -> Result<ContactDto, DomainError>;
async fn search_contacts(&self, address_book_id: &str, query: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError>; async fn list_contacts(
&self,
address_book_id: &str,
user_id: &str,
) -> Result<Vec<ContactDto>, DomainError>;
async fn search_contacts(
&self,
address_book_id: &str,
query: &str,
user_id: &str,
) -> Result<Vec<ContactDto>, DomainError>;
// Contact Group operations // Contact Group operations
async fn create_group(&self, dto: CreateContactGroupDto) -> Result<ContactGroupDto, DomainError>; async fn create_group(
async fn update_group(&self, group_id: &str, update: UpdateContactGroupDto) -> Result<ContactGroupDto, DomainError>; &self,
dto: CreateContactGroupDto,
) -> Result<ContactGroupDto, DomainError>;
async fn update_group(
&self,
group_id: &str,
update: UpdateContactGroupDto,
) -> Result<ContactGroupDto, DomainError>;
async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError>; async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn get_group(&self, group_id: &str, user_id: &str) -> Result<ContactGroupDto, DomainError>; async fn get_group(
async fn list_groups(&self, address_book_id: &str, user_id: &str) -> Result<Vec<ContactGroupDto>, DomainError>; &self,
group_id: &str,
user_id: &str,
) -> Result<ContactGroupDto, DomainError>;
async fn list_groups(
&self,
address_book_id: &str,
user_id: &str,
) -> Result<Vec<ContactGroupDto>, DomainError>;
// Group membership // Group membership
async fn add_contact_to_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError>; async fn add_contact_to_group(
async fn remove_contact_from_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError>; &self,
async fn list_contacts_in_group(&self, group_id: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError>; dto: GroupMembershipDto,
async fn list_groups_for_contact(&self, contact_id: &str, user_id: &str) -> Result<Vec<ContactGroupDto>, DomainError>; user_id: &str,
) -> Result<(), DomainError>;
async fn remove_contact_from_group(
&self,
dto: GroupMembershipDto,
user_id: &str,
) -> Result<(), DomainError>;
async fn list_contacts_in_group(
&self,
group_id: &str,
user_id: &str,
) -> Result<Vec<ContactDto>, DomainError>;
async fn list_groups_for_contact(
&self,
contact_id: &str,
user_id: &str,
) -> Result<Vec<ContactGroupDto>, DomainError>;
// vCard operations // vCard operations
async fn get_contact_vcard(&self, contact_id: &str, user_id: &str) -> Result<String, DomainError>; async fn get_contact_vcard(
async fn get_contacts_as_vcards(&self, address_book_id: &str, user_id: &str) -> Result<Vec<(String, String)>, DomainError>; &self,
} contact_id: &str,
user_id: &str,
) -> Result<String, DomainError>;
async fn get_contacts_as_vcards(
&self,
address_book_id: &str,
user_id: &str,
) -> Result<Vec<(String, String)>, DomainError>;
}
+5 -14
View File
@@ -4,11 +4,11 @@
//! operations, keeping the application and interface layers independent of //! operations, keeping the application and interface layers independent of
//! the specific upload implementation (TUS-like protocol, S3 multipart, etc.). //! the specific upload implementation (TUS-like protocol, S3 multipart, etc.).
use std::path::PathBuf; use crate::common::errors::DomainError;
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use serde::Serialize; use serde::Serialize;
use crate::common::errors::DomainError; use std::path::PathBuf;
/// Default chunk size (5 MB) — optimised for parallel transfers. /// Default chunk size (5 MB) — optimised for parallel transfers.
pub const DEFAULT_CHUNK_SIZE: usize = 5 * 1024 * 1024; pub const DEFAULT_CHUNK_SIZE: usize = 5 * 1024 * 1024;
@@ -80,10 +80,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
) -> Result<ChunkUploadResponseDto, DomainError>; ) -> Result<ChunkUploadResponseDto, DomainError>;
/// Get the current status of an upload session. /// Get the current status of an upload session.
async fn get_status( async fn get_status(&self, upload_id: &str) -> Result<UploadStatusResponseDto, DomainError>;
&self,
upload_id: &str,
) -> Result<UploadStatusResponseDto, DomainError>;
/// Assemble all chunks into the final file. /// Assemble all chunks into the final file.
/// ///
@@ -94,16 +91,10 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
) -> Result<(PathBuf, String, Option<String>, String, u64), DomainError>; ) -> Result<(PathBuf, String, Option<String>, String, u64), DomainError>;
/// Finalize upload: clean up the session and temporary files. /// Finalize upload: clean up the session and temporary files.
async fn finalize_upload( async fn finalize_upload(&self, upload_id: &str) -> Result<(), DomainError>;
&self,
upload_id: &str,
) -> Result<(), DomainError>;
/// Cancel an upload and clean up all temporary data. /// Cancel an upload and clean up all temporary data.
async fn cancel_upload( async fn cancel_upload(&self, upload_id: &str) -> Result<(), DomainError>;
&self,
upload_id: &str,
) -> Result<(), DomainError>;
/// Check if a file size qualifies for chunked upload. /// Check if a file size qualifies for chunked upload.
fn should_use_chunked(&self, size: u64) -> bool; fn should_use_chunked(&self, size: u64) -> bool;
+6 -2
View File
@@ -4,8 +4,8 @@
//! keeping the application and interface layers independent of specific //! keeping the application and interface layers independent of specific
//! compression implementations (gzip, zstd, etc.). //! compression implementations (gzip, zstd, etc.).
use async_trait::async_trait;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use async_trait::async_trait;
/// Compression level settings for file compression operations. /// Compression level settings for file compression operations.
/// ///
@@ -30,7 +30,11 @@ pub enum CompressionLevel {
#[async_trait] #[async_trait]
pub trait CompressionPort: Send + Sync + 'static { pub trait CompressionPort: Send + Sync + 'static {
/// Compress data in memory. /// Compress data in memory.
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> Result<Vec<u8>, DomainError>; async fn compress_data(
&self,
data: &[u8],
level: CompressionLevel,
) -> Result<Vec<u8>, DomainError>;
/// Decompress data in memory. /// Decompress data in memory.
async fn decompress_data(&self, compressed_data: &[u8]) -> Result<Vec<u8>, DomainError>; async fn decompress_data(&self, compressed_data: &[u8]) -> Result<Vec<u8>, DomainError>;
+2 -2
View File
@@ -4,11 +4,11 @@
//! keeping the application and interface layers independent of the specific //! keeping the application and interface layers independent of the specific
//! content-addressable storage implementation. //! content-addressable storage implementation.
use std::path::{Path, PathBuf}; use crate::common::errors::DomainError;
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use serde::Serialize; use serde::Serialize;
use crate::common::errors::DomainError; use std::path::{Path, PathBuf};
/// Metadata of a stored blob in the dedup system. /// Metadata of a stored blob in the dedup system.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
+12 -7
View File
@@ -1,19 +1,24 @@
use async_trait::async_trait;
use crate::common::errors::Result;
use crate::application::dtos::favorites_dto::FavoriteItemDto; use crate::application::dtos::favorites_dto::FavoriteItemDto;
use crate::common::errors::Result;
use async_trait::async_trait;
/// Defines operations for managing user favorites /// Defines operations for managing user favorites
#[async_trait] #[async_trait]
pub trait FavoritesUseCase: Send + Sync { pub trait FavoritesUseCase: Send + Sync {
/// Get all favorites for a user /// Get all favorites for a user
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>>; async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>>;
/// Add an item to user's favorites /// Add an item to user's favorites
async fn add_to_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>; async fn add_to_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
/// Remove an item from user's favorites /// Remove an item from user's favorites
async fn remove_from_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>; async fn remove_from_favorites(
&self,
user_id: &str,
item_id: &str,
item_type: &str,
) -> Result<bool>;
/// Check if an item is in user's favorites /// Check if an item is in user's favorites
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>; async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
} }
@@ -40,4 +45,4 @@ pub trait FavoritesRepositoryPort: Send + Sync + 'static {
/// Checks if an item is in favorites. /// Checks if an item is in favorites.
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>; async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
} }
+26 -17
View File
@@ -1,8 +1,8 @@
use std::sync::Arc;
use std::pin::Pin;
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use futures::Stream; use futures::Stream;
use std::pin::Pin;
use std::sync::Arc;
use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::file_dto::FileDto;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
@@ -48,7 +48,13 @@ pub trait FileUploadUseCase: Send + Sync + 'static {
) -> Result<(FileDto, UploadStrategy), DomainError>; ) -> Result<(FileDto, UploadStrategy), DomainError>;
/// Creates a new file at the specified path (for WebDAV) /// Creates a new file at the specified path (for WebDAV)
async fn create_file(&self, parent_path: &str, filename: &str, content: &[u8], content_type: &str) -> Result<FileDto, DomainError>; async fn create_file(
&self,
parent_path: &str,
filename: &str,
content: &[u8],
content_type: &str,
) -> Result<FileDto, DomainError>;
/// Updates the content of an existing file (for WebDAV) /// Updates the content of an existing file (for WebDAV)
async fn update_file(&self, path: &str, content: &[u8]) -> Result<(), DomainError>; async fn update_file(&self, path: &str, content: &[u8]) -> Result<(), DomainError>;
@@ -81,18 +87,21 @@ pub enum OptimizedFileContent {
pub trait FileRetrievalUseCase: Send + Sync + 'static { pub trait FileRetrievalUseCase: Send + Sync + 'static {
/// Gets a file by its ID /// Gets a file by its ID
async fn get_file(&self, id: &str) -> Result<FileDto, DomainError>; async fn get_file(&self, id: &str) -> Result<FileDto, DomainError>;
/// Gets a file by its path (for WebDAV) /// Gets a file by its path (for WebDAV)
async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>; async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>;
/// Lists files in a folder /// Lists files in a folder
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>; async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>;
/// Gets file content as bytes (for small files) /// Gets file content as bytes (for small files)
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>; async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
/// Gets file content as a stream (for large files) /// Gets file content as a stream (for large files)
async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>; async fn get_file_stream(
&self,
id: &str,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
/// Optimized multi-tier download. /// Optimized multi-tier download.
/// ///
@@ -123,11 +132,15 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
#[async_trait] #[async_trait]
pub trait FileManagementUseCase: Send + Sync + 'static { pub trait FileManagementUseCase: Send + Sync + 'static {
/// Moves a file to another folder /// Moves a file to another folder
async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError>; async fn move_file(
&self,
file_id: &str,
folder_id: Option<String>,
) -> Result<FileDto, DomainError>;
/// Renames a file /// Renames a file
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<FileDto, DomainError>; async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<FileDto, DomainError>;
/// Deletes a file /// Deletes a file
async fn delete_file(&self, id: &str) -> Result<(), DomainError>; async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
@@ -138,11 +151,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
/// 3. Decrements the dedup reference count for the content hash. /// 3. Decrements the dedup reference count for the content hash.
/// ///
/// Returns `Ok(true)` when trashed, `Ok(false)` when permanently deleted. /// Returns `Ok(true)` when trashed, `Ok(false)` when permanently deleted.
async fn delete_with_cleanup( async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result<bool, DomainError>;
&self,
id: &str,
user_id: &str,
) -> Result<bool, DomainError>;
} }
/// Factory for creating file use case implementations /// Factory for creating file use case implementations
@@ -150,4 +159,4 @@ pub trait FileUseCaseFactory: Send + Sync + 'static {
fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase>; fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase>;
fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase>; fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase>;
fn create_file_management_use_case(&self) -> Arc<dyn FileManagementUseCase>; fn create_file_management_use_case(&self) -> Arc<dyn FileManagementUseCase>;
} }
+19 -16
View File
@@ -1,6 +1,8 @@
use async_trait::async_trait; use async_trait::async_trait;
use crate::application::dtos::folder_dto::{CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto}; use crate::application::dtos::folder_dto::{
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
};
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto}; use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
@@ -9,36 +11,37 @@ use crate::common::errors::DomainError;
pub trait FolderUseCase: Send + Sync + 'static { pub trait FolderUseCase: Send + Sync + 'static {
/// Creates a new folder /// Creates a new folder
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError>; async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError>;
/// Gets a folder by its ID /// Gets a folder by its ID
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError>; async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError>;
/// Gets a folder by its path /// Gets a folder by its path
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>; async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>;
/// Lists folders within a parent folder /// Lists folders within a parent folder
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError>; async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError>;
/// Lists folders with pagination /// Lists folders with pagination
async fn list_folders_paginated( async fn list_folders_paginated(
&self, &self,
parent_id: Option<&str>, parent_id: Option<&str>,
pagination: &crate::application::dtos::pagination::PaginationRequestDto pagination: &crate::application::dtos::pagination::PaginationRequestDto,
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>; ) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
/// Renames a folder /// Renames a folder
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError>; async fn rename_folder(&self, id: &str, dto: RenameFolderDto)
-> Result<FolderDto, DomainError>;
/// Moves a folder to another parent /// Moves a folder to another parent
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError>; async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError>;
/// Deletes a folder /// Deletes a folder
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>; async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
} }
/** /**
* Primary port for file and folder search * Primary port for file and folder search
* *
* Defines the operations related to advanced search of * Defines the operations related to advanced search of
* files and folders based on various criteria. * files and folders based on various criteria.
*/ */
@@ -46,16 +49,16 @@ pub trait FolderUseCase: Send + Sync + 'static {
pub trait SearchUseCase: Send + Sync + 'static { pub trait SearchUseCase: Send + Sync + 'static {
/** /**
* Performs a search based on the specified criteria * Performs a search based on the specified criteria
* *
* @param criteria Search criteria including text, dates, sizes, etc. * @param criteria Search criteria including text, dates, sizes, etc.
* @return Search results containing matching files and folders * @return Search results containing matching files and folders
*/ */
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto, DomainError>; async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto, DomainError>;
/** /**
* Clears the search results cache * Clears the search results cache
* *
* @return Result indicating success or error * @return Result indicating success or error
*/ */
async fn clear_search_cache(&self) -> Result<(), DomainError>; async fn clear_search_cache(&self) -> Result<(), DomainError>;
} }
+1 -1
View File
@@ -15,4 +15,4 @@ pub mod storage_ports;
pub mod thumbnail_ports; pub mod thumbnail_ports;
pub mod transcode_ports; pub mod transcode_ports;
pub mod trash_ports; pub mod trash_ports;
pub mod zip_ports; pub mod zip_ports;
+12 -12
View File
@@ -1,8 +1,8 @@
use std::path::PathBuf;
use async_trait::async_trait; use async_trait::async_trait;
use std::path::PathBuf;
use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use crate::domain::services::path_service::StoragePath;
// Re-export domain repository traits for backward compatibility // Re-export domain repository traits for backward compatibility
pub use crate::domain::repositories::folder_repository::FolderRepository; pub use crate::domain::repositories::folder_repository::FolderRepository;
@@ -14,13 +14,13 @@ use super::storage_ports::{FileReadPort, FileWritePort};
pub trait StoragePort: Send + Sync + 'static { pub trait StoragePort: Send + Sync + 'static {
/// Resolves a domain path to a physical path /// Resolves a domain path to a physical path
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf; fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf;
/// Creates directories if they don't exist /// Creates directories if they don't exist
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>; async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
/// Checks if a file exists at the given path /// Checks if a file exists at the given path
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>; async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
/// Checks if a directory exists at the given path /// Checks if a directory exists at the given path
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>; async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
} }
@@ -54,28 +54,28 @@ impl<T: FolderRepository> FolderStoragePort for T {}
pub trait IdMappingPort: Send + Sync + 'static { pub trait IdMappingPort: Send + Sync + 'static {
/// Gets or creates an ID for a path /// Gets or creates an ID for a path
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError>; async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError>;
/// Gets a path by its ID /// Gets a path by its ID
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError>; async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError>;
/// Updates the path for an existing ID /// Updates the path for an existing ID
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError>; async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError>;
/// Removes an ID from the mapping /// Removes an ID from the mapping
async fn remove_id(&self, id: &str) -> Result<(), DomainError>; async fn remove_id(&self, id: &str) -> Result<(), DomainError>;
/// Saves pending changes /// Saves pending changes
async fn save_changes(&self) -> Result<(), DomainError>; async fn save_changes(&self) -> Result<(), DomainError>;
/// Gets the file path as a PathBuf /// Gets the file path as a PathBuf
async fn get_file_path(&self, file_id: &str) -> Result<PathBuf, DomainError> { async fn get_file_path(&self, file_id: &str) -> Result<PathBuf, DomainError> {
let storage_path = self.get_path_by_id(file_id).await?; let storage_path = self.get_path_by_id(file_id).await?;
Ok(PathBuf::from(storage_path.to_string())) Ok(PathBuf::from(storage_path.to_string()))
} }
/// Updates a file's path /// Updates a file's path
async fn update_file_path(&self, file_id: &str, new_path: &PathBuf) -> Result<(), DomainError> { async fn update_file_path(&self, file_id: &str, new_path: &PathBuf) -> Result<(), DomainError> {
let storage_path = StoragePath::from_string(new_path.to_string_lossy().as_ref()); let storage_path = StoragePath::from_string(new_path.to_string_lossy().as_ref());
self.update_path(file_id, &storage_path).await self.update_path(file_id, &storage_path).await
} }
} }
+19 -9
View File
@@ -1,19 +1,29 @@
use async_trait::async_trait;
use crate::common::errors::Result;
use crate::application::dtos::recent_dto::RecentItemDto; use crate::application::dtos::recent_dto::RecentItemDto;
use crate::common::errors::Result;
use async_trait::async_trait;
/// Defines operations for managing user recent items /// Defines operations for managing user recent items
#[async_trait] #[async_trait]
pub trait RecentItemsUseCase: Send + Sync { pub trait RecentItemsUseCase: Send + Sync {
/// Get all recent items for a user /// Get all recent items for a user
async fn get_recent_items(&self, user_id: &str, limit: Option<i32>) -> Result<Vec<RecentItemDto>>; async fn get_recent_items(
&self,
user_id: &str,
limit: Option<i32>,
) -> Result<Vec<RecentItemDto>>;
/// Record access to an item /// Record access to an item
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>; async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str)
-> Result<()>;
/// Remove an item from recents /// Remove an item from recents
async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>; async fn remove_from_recent(
&self,
user_id: &str,
item_id: &str,
item_type: &str,
) -> Result<bool>;
/// Clear the entire recent items list /// Clear the entire recent items list
async fn clear_recent_items(&self, user_id: &str) -> Result<()>; async fn clear_recent_items(&self, user_id: &str) -> Result<()>;
} }
@@ -42,4 +52,4 @@ pub trait RecentItemsRepositoryPort: Send + Sync + 'static {
/// Removes items exceeding `max_items` (the oldest ones). /// Removes items exceeding `max_items` (the oldest ones).
async fn prune(&self, user_id: &str, max_items: i32) -> Result<()>; async fn prune(&self, user_id: &str, max_items: i32) -> Result<()>;
} }
+35 -21
View File
@@ -3,13 +3,12 @@ use async_trait::async_trait;
use crate::{ use crate::{
application::dtos::{ application::dtos::{
pagination::PaginatedResponseDto, pagination::PaginatedResponseDto,
share_dto::{CreateShareDto, ShareDto, UpdateShareDto} share_dto::{CreateShareDto, ShareDto, UpdateShareDto},
}, },
common::errors::DomainError, common::errors::DomainError,
domain::entities::share::ShareItemType, domain::entities::share::ShareItemType,
}; };
#[async_trait] #[async_trait]
pub trait ShareUseCase: Send + Sync + 'static { pub trait ShareUseCase: Send + Sync + 'static {
/// Create a new shared link for a file or folder /// Create a new shared link for a file or folder
@@ -56,30 +55,45 @@ pub trait ShareUseCase: Send + Sync + 'static {
token: &str, token: &str,
password: &str, password: &str,
) -> Result<bool, DomainError>; ) -> Result<bool, DomainError>;
/// Register an access to a shared link /// Register an access to a shared link
async fn register_shared_link_access(&self, token: &str) -> Result<(), DomainError>; async fn register_shared_link_access(&self, token: &str) -> Result<(), DomainError>;
} }
#[async_trait] #[async_trait]
pub trait ShareStoragePort: Send + Sync + 'static { pub trait ShareStoragePort: Send + Sync + 'static {
async fn save_share(&self, share: &crate::domain::entities::share::Share) async fn save_share(
-> Result<crate::domain::entities::share::Share, DomainError>; &self,
share: &crate::domain::entities::share::Share,
async fn find_share_by_id(&self, id: &str) ) -> Result<crate::domain::entities::share::Share, DomainError>;
-> Result<crate::domain::entities::share::Share, DomainError>;
async fn find_share_by_id(
async fn find_share_by_token(&self, token: &str) &self,
-> Result<crate::domain::entities::share::Share, DomainError>; id: &str,
) -> Result<crate::domain::entities::share::Share, DomainError>;
async fn find_shares_by_item(&self, item_id: &str, item_type: &ShareItemType)
-> Result<Vec<crate::domain::entities::share::Share>, DomainError>; async fn find_share_by_token(
&self,
async fn update_share(&self, share: &crate::domain::entities::share::Share) token: &str,
-> Result<crate::domain::entities::share::Share, DomainError>; ) -> Result<crate::domain::entities::share::Share, DomainError>;
async fn find_shares_by_item(
&self,
item_id: &str,
item_type: &ShareItemType,
) -> Result<Vec<crate::domain::entities::share::Share>, DomainError>;
async fn update_share(
&self,
share: &crate::domain::entities::share::Share,
) -> Result<crate::domain::entities::share::Share, DomainError>;
async fn delete_share(&self, id: &str) -> Result<(), DomainError>; async fn delete_share(&self, id: &str) -> Result<(), DomainError>;
async fn find_shares_by_user(&self, user_id: &str, offset: usize, limit: usize) async fn find_shares_by_user(
-> Result<(Vec<crate::domain::entities::share::Share>, usize), DomainError>; &self,
user_id: &str,
offset: usize,
limit: usize,
) -> Result<(Vec<crate::domain::entities::share::Share>, usize), DomainError>;
} }
+14 -11
View File
@@ -1,16 +1,18 @@
use std::path::PathBuf;
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use futures::Stream; use futures::Stream;
use serde_json::Value; use serde_json::Value;
use std::path::PathBuf;
use crate::common::errors::DomainError;
use crate::domain::entities::file::File; use crate::domain::entities::file::File;
use crate::domain::services::path_service::StoragePath; use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError;
// Re-export domain repository traits for backward compatibility. // Re-export domain repository traits for backward compatibility.
// The canonical definitions now live in domain/repositories/. // The canonical definitions now live in domain/repositories/.
pub use crate::domain::repositories::file_repository::{FileReadRepository, FileWriteRepository, FileRepository}; pub use crate::domain::repositories::file_repository::{
FileReadRepository, FileRepository, FileWriteRepository,
};
pub use crate::domain::repositories::folder_repository::FolderRepository; pub use crate::domain::repositories::folder_repository::FolderRepository;
// ───────────────────────────────────────────────────── // ─────────────────────────────────────────────────────
@@ -92,17 +94,14 @@ pub trait FileWritePort: Send + Sync + 'static {
) -> Result<File, DomainError>; ) -> Result<File, DomainError>;
/// Renames a file (same folder, different name). /// Renames a file (same folder, different name).
async fn rename_file( async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<File, DomainError>;
&self,
file_id: &str,
new_name: &str,
) -> Result<File, DomainError>;
/// Deletes a file. /// Deletes a file.
async fn delete_file(&self, id: &str) -> Result<(), DomainError>; async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
/// Updates the content of an existing file. /// Updates the content of an existing file.
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError>; async fn update_file_content(&self, file_id: &str, content: Vec<u8>)
-> Result<(), DomainError>;
/// Registers file metadata WITHOUT writing content to disk (write-behind). /// Registers file metadata WITHOUT writing content to disk (write-behind).
/// ///
@@ -122,7 +121,11 @@ pub trait FileWritePort: Send + Sync + 'static {
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>; async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
/// Restores a file from the trash to its original location /// Restores a file from the trash to its original location
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>; async fn restore_from_trash(
&self,
file_id: &str,
original_path: &str,
) -> Result<(), DomainError>;
/// Permanently deletes a file (used by the trash) /// Permanently deletes a file (used by the trash)
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>; async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
@@ -174,4 +177,4 @@ pub trait StorageUsagePort: Send + Sync + 'static {
pub trait StorageUseCase: Send + Sync + 'static { pub trait StorageUseCase: Send + Sync + 'static {
/// Handle a request with the specified action and parameters /// Handle a request with the specified action and parameters
async fn handle_request(&self, action: &str, params: Value) -> Result<Value, DomainError>; async fn handle_request(&self, action: &str, params: Value) -> Result<Value, DomainError>;
} }
+9 -9
View File
@@ -4,11 +4,11 @@
//! keeping the application and interface layers independent of specific //! keeping the application and interface layers independent of specific
//! image processing implementations. //! image processing implementations.
use std::path::{Path, PathBuf}; use crate::common::errors::DomainError;
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use crate::common::errors::DomainError; use std::path::{Path, PathBuf};
use std::sync::Arc;
/// Thumbnail sizes supported by the system. /// Thumbnail sizes supported by the system.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -42,7 +42,11 @@ impl ThumbnailSize {
/// Get all thumbnail sizes. /// Get all thumbnail sizes.
pub fn all() -> &'static [ThumbnailSize] { pub fn all() -> &'static [ThumbnailSize] {
&[ThumbnailSize::Icon, ThumbnailSize::Preview, ThumbnailSize::Large] &[
ThumbnailSize::Icon,
ThumbnailSize::Preview,
ThumbnailSize::Large,
]
} }
} }
@@ -77,11 +81,7 @@ pub trait ThumbnailPort: Send + Sync + 'static {
/// Generate all thumbnail sizes for a file in the background. /// Generate all thumbnail sizes for a file in the background.
/// ///
/// Called after file upload to pre-generate thumbnails. /// Called after file upload to pre-generate thumbnails.
fn generate_all_sizes_background( fn generate_all_sizes_background(self: Arc<Self>, file_id: String, original_path: PathBuf);
self: Arc<Self>,
file_id: String,
original_path: PathBuf,
);
/// Delete all thumbnails for a file. /// Delete all thumbnails for a file.
async fn delete_thumbnails(&self, file_id: &str) -> Result<(), DomainError>; async fn delete_thumbnails(&self, file_id: &str) -> Result<(), DomainError>;
+1 -1
View File
@@ -4,9 +4,9 @@
//! (e.g., JPEG/PNG → WebP), keeping the application and interface layers //! (e.g., JPEG/PNG → WebP), keeping the application and interface layers
//! independent of specific image processing implementations. //! independent of specific image processing implementations.
use crate::common::errors::DomainError;
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use crate::common::errors::DomainError;
/// Supported output formats for image transcoding. /// Supported output formats for image transcoding.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+5 -5
View File
@@ -8,16 +8,16 @@ use crate::common::errors::Result;
pub trait TrashUseCase: Send + Sync { pub trait TrashUseCase: Send + Sync {
/// List items in the user's trash /// List items in the user's trash
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>>; async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>>;
/// Move a file or folder to trash /// Move a file or folder to trash
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()>; async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()>;
/// Restore an item from trash to its original location /// Restore an item from trash to its original location
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()>; async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()>;
/// Permanently delete an item from trash /// Permanently delete an item from trash
async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()>; async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()>;
/// Empty the trash for a specific user /// Empty the trash for a specific user
async fn empty_trash(&self, user_id: &str) -> Result<()>; async fn empty_trash(&self, user_id: &str) -> Result<()>;
} }
+1 -1
View File
@@ -4,8 +4,8 @@
//! keeping the interface layer independent of specific ZIP //! keeping the interface layer independent of specific ZIP
//! implementation details. //! implementation details.
use async_trait::async_trait;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use async_trait::async_trait;
/// Port for ZIP archive operations. /// Port for ZIP archive operations.
/// ///
+398 -302
View File
@@ -1,302 +1,398 @@
use std::sync::Arc; use std::sync::Arc;
use crate::domain::repositories::settings_repository::SettingsRepository; use crate::application::dtos::settings_dto::{
use crate::application::services::auth_application_service::AuthApplicationService; OidcSettingsDto, OidcTestResultDto, SaveOidcSettingsDto, TestOidcConnectionDto,
use crate::application::dtos::settings_dto::{ };
OidcSettingsDto, SaveOidcSettingsDto, OidcTestResultDto, TestOidcConnectionDto, use crate::application::services::auth_application_service::AuthApplicationService;
}; use crate::common::config::OidcConfig;
use crate::infrastructure::services::oidc_service::OidcService; use crate::common::errors::{DomainError, ErrorKind};
use crate::common::config::OidcConfig; use crate::domain::repositories::settings_repository::SettingsRepository;
use crate::common::errors::{DomainError, ErrorKind}; use crate::infrastructure::services::oidc_service::OidcService;
/// Admin settings service — manages platform configuration in the database. /// Admin settings service — manages platform configuration in the database.
/// ///
/// Configuration priority: **env vars > DB settings > defaults**. /// Configuration priority: **env vars > DB settings > defaults**.
/// Supports hot-reloading OIDC configuration without server restart. /// Supports hot-reloading OIDC configuration without server restart.
pub struct AdminSettingsService { pub struct AdminSettingsService {
settings_repo: Arc<dyn SettingsRepository>, settings_repo: Arc<dyn SettingsRepository>,
env_oidc_config: OidcConfig, env_oidc_config: OidcConfig,
auth_app_service: Arc<AuthApplicationService>, auth_app_service: Arc<AuthApplicationService>,
server_base_url: String, server_base_url: String,
} }
impl AdminSettingsService { impl AdminSettingsService {
pub fn new( pub fn new(
settings_repo: Arc<dyn SettingsRepository>, settings_repo: Arc<dyn SettingsRepository>,
env_oidc_config: OidcConfig, env_oidc_config: OidcConfig,
auth_app_service: Arc<AuthApplicationService>, auth_app_service: Arc<AuthApplicationService>,
server_base_url: String, server_base_url: String,
) -> Self { ) -> Self {
Self { Self {
settings_repo, settings_repo,
env_oidc_config, env_oidc_config,
auth_app_service, auth_app_service,
server_base_url, server_base_url,
} }
} }
/// Auto-generated OIDC callback URL /// Auto-generated OIDC callback URL
fn callback_url(&self) -> String { fn callback_url(&self) -> String {
let base = self.server_base_url.trim_end_matches('/'); let base = self.server_base_url.trim_end_matches('/');
format!("{}/api/auth/oidc/callback", base) format!("{}/api/auth/oidc/callback", base)
} }
/// Detect which OIDC fields are overridden by environment variables /// Detect which OIDC fields are overridden by environment variables
fn get_env_overrides(&self) -> Vec<String> { fn get_env_overrides(&self) -> Vec<String> {
let mut out = Vec::new(); let mut out = Vec::new();
let vars = [ let vars = [
("OXICLOUD_OIDC_ENABLED", "enabled"), ("OXICLOUD_OIDC_ENABLED", "enabled"),
("OXICLOUD_OIDC_ISSUER_URL", "issuer_url"), ("OXICLOUD_OIDC_ISSUER_URL", "issuer_url"),
("OXICLOUD_OIDC_CLIENT_ID", "client_id"), ("OXICLOUD_OIDC_CLIENT_ID", "client_id"),
("OXICLOUD_OIDC_CLIENT_SECRET", "client_secret"), ("OXICLOUD_OIDC_CLIENT_SECRET", "client_secret"),
("OXICLOUD_OIDC_SCOPES", "scopes"), ("OXICLOUD_OIDC_SCOPES", "scopes"),
("OXICLOUD_OIDC_AUTO_PROVISION", "auto_provision"), ("OXICLOUD_OIDC_AUTO_PROVISION", "auto_provision"),
("OXICLOUD_OIDC_ADMIN_GROUPS", "admin_groups"), ("OXICLOUD_OIDC_ADMIN_GROUPS", "admin_groups"),
("OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN", "disable_password_login"), (
("OXICLOUD_OIDC_PROVIDER_NAME", "provider_name"), "OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN",
]; "disable_password_login",
for (env_key, field_name) in &vars { ),
if std::env::var(env_key).is_ok() { ("OXICLOUD_OIDC_PROVIDER_NAME", "provider_name"),
out.push(field_name.to_string()); ];
} for (env_key, field_name) in &vars {
} if std::env::var(env_key).is_ok() {
out out.push(field_name.to_string());
} }
}
/// Apply environment variable overrides on top of a config out
fn apply_env_overrides(&self, config: &mut OidcConfig) { }
let e = &self.env_oidc_config;
if std::env::var("OXICLOUD_OIDC_ENABLED").is_ok() { config.enabled = e.enabled; } /// Apply environment variable overrides on top of a config
if std::env::var("OXICLOUD_OIDC_ISSUER_URL").is_ok() { config.issuer_url = e.issuer_url.clone(); } fn apply_env_overrides(&self, config: &mut OidcConfig) {
if std::env::var("OXICLOUD_OIDC_CLIENT_ID").is_ok() { config.client_id = e.client_id.clone(); } let e = &self.env_oidc_config;
if std::env::var("OXICLOUD_OIDC_CLIENT_SECRET").is_ok() { config.client_secret = e.client_secret.clone(); } if std::env::var("OXICLOUD_OIDC_ENABLED").is_ok() {
if std::env::var("OXICLOUD_OIDC_SCOPES").is_ok() { config.scopes = e.scopes.clone(); } config.enabled = e.enabled;
if std::env::var("OXICLOUD_OIDC_REDIRECT_URI").is_ok() { config.redirect_uri = e.redirect_uri.clone(); } }
if std::env::var("OXICLOUD_OIDC_FRONTEND_URL").is_ok() { config.frontend_url = e.frontend_url.clone(); } if std::env::var("OXICLOUD_OIDC_ISSUER_URL").is_ok() {
if std::env::var("OXICLOUD_OIDC_AUTO_PROVISION").is_ok() { config.auto_provision = e.auto_provision; } config.issuer_url = e.issuer_url.clone();
if std::env::var("OXICLOUD_OIDC_ADMIN_GROUPS").is_ok() { config.admin_groups = e.admin_groups.clone(); } }
if std::env::var("OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN").is_ok() { config.disable_password_login = e.disable_password_login; } if std::env::var("OXICLOUD_OIDC_CLIENT_ID").is_ok() {
if std::env::var("OXICLOUD_OIDC_PROVIDER_NAME").is_ok() { config.provider_name = e.provider_name.clone(); } config.client_id = e.client_id.clone();
} }
if std::env::var("OXICLOUD_OIDC_CLIENT_SECRET").is_ok() {
/// Load the effective OIDC config: DB settings + env var overrides + defaults. config.client_secret = e.client_secret.clone();
pub async fn load_effective_oidc_config(&self) -> Result<OidcConfig, DomainError> { }
let db = self.settings_repo.get_by_category("oidc").await?; if std::env::var("OXICLOUD_OIDC_SCOPES").is_ok() {
let d = OidcConfig::default(); config.scopes = e.scopes.clone();
}
let mut config = OidcConfig { if std::env::var("OXICLOUD_OIDC_REDIRECT_URI").is_ok() {
enabled: db.get("oidc.enabled").and_then(|v| v.parse().ok()).unwrap_or(d.enabled), config.redirect_uri = e.redirect_uri.clone();
issuer_url: db.get("oidc.issuer_url").cloned().unwrap_or(d.issuer_url), }
client_id: db.get("oidc.client_id").cloned().unwrap_or(d.client_id), if std::env::var("OXICLOUD_OIDC_FRONTEND_URL").is_ok() {
client_secret: db.get("oidc.client_secret").cloned().unwrap_or(d.client_secret), config.frontend_url = e.frontend_url.clone();
redirect_uri: self.callback_url(), }
scopes: db.get("oidc.scopes").cloned().unwrap_or(d.scopes), if std::env::var("OXICLOUD_OIDC_AUTO_PROVISION").is_ok() {
frontend_url: self.server_base_url.clone(), config.auto_provision = e.auto_provision;
auto_provision: db.get("oidc.auto_provision").and_then(|v| v.parse().ok()).unwrap_or(d.auto_provision), }
admin_groups: db.get("oidc.admin_groups").cloned().unwrap_or(d.admin_groups), if std::env::var("OXICLOUD_OIDC_ADMIN_GROUPS").is_ok() {
disable_password_login: db.get("oidc.disable_password_login").and_then(|v| v.parse().ok()).unwrap_or(d.disable_password_login), config.admin_groups = e.admin_groups.clone();
provider_name: db.get("oidc.provider_name").cloned().unwrap_or(d.provider_name), }
}; if std::env::var("OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN").is_ok() {
config.disable_password_login = e.disable_password_login;
// Env vars override DB }
self.apply_env_overrides(&mut config); if std::env::var("OXICLOUD_OIDC_PROVIDER_NAME").is_ok() {
Ok(config) config.provider_name = e.provider_name.clone();
} }
}
/// Get OIDC settings for display in admin UI (secrets masked).
pub async fn get_oidc_settings(&self) -> Result<OidcSettingsDto, DomainError> { /// Load the effective OIDC config: DB settings + env var overrides + defaults.
let db = self.settings_repo.get_by_category("oidc").await?; pub async fn load_effective_oidc_config(&self) -> Result<OidcConfig, DomainError> {
let d = OidcConfig::default(); let db = self.settings_repo.get_by_category("oidc").await?;
let d = OidcConfig::default();
let has_secret = db.get("oidc.client_secret").map(|s| !s.is_empty()).unwrap_or(false)
|| std::env::var("OXICLOUD_OIDC_CLIENT_SECRET").map(|s| !s.is_empty()).unwrap_or(false); let mut config = OidcConfig {
enabled: db
Ok(OidcSettingsDto { .get("oidc.enabled")
enabled: db.get("oidc.enabled").and_then(|v| v.parse().ok()).unwrap_or(d.enabled), .and_then(|v| v.parse().ok())
issuer_url: db.get("oidc.issuer_url").cloned().unwrap_or_default(), .unwrap_or(d.enabled),
client_id: db.get("oidc.client_id").cloned().unwrap_or_default(), issuer_url: db.get("oidc.issuer_url").cloned().unwrap_or(d.issuer_url),
client_secret_set: has_secret, client_id: db.get("oidc.client_id").cloned().unwrap_or(d.client_id),
scopes: db.get("oidc.scopes").cloned().unwrap_or(d.scopes), client_secret: db
auto_provision: db.get("oidc.auto_provision").and_then(|v| v.parse().ok()).unwrap_or(d.auto_provision), .get("oidc.client_secret")
admin_groups: db.get("oidc.admin_groups").cloned().unwrap_or_default(), .cloned()
disable_password_login: db.get("oidc.disable_password_login").and_then(|v| v.parse().ok()).unwrap_or(d.disable_password_login), .unwrap_or(d.client_secret),
provider_name: db.get("oidc.provider_name").cloned().unwrap_or(d.provider_name), redirect_uri: self.callback_url(),
callback_url: self.callback_url(), scopes: db.get("oidc.scopes").cloned().unwrap_or(d.scopes),
env_overrides: self.get_env_overrides(), frontend_url: self.server_base_url.clone(),
}) auto_provision: db
} .get("oidc.auto_provision")
.and_then(|v| v.parse().ok())
/// Save OIDC settings to DB and hot-reload the OIDC service. .unwrap_or(d.auto_provision),
pub async fn save_oidc_settings( admin_groups: db
&self, .get("oidc.admin_groups")
dto: SaveOidcSettingsDto, .cloned()
updated_by: &str, .unwrap_or(d.admin_groups),
) -> Result<(), DomainError> { disable_password_login: db
let cat = "oidc"; .get("oidc.disable_password_login")
let by = Some(updated_by); .and_then(|v| v.parse().ok())
.unwrap_or(d.disable_password_login),
self.settings_repo.set("oidc.enabled", &dto.enabled.to_string(), cat, false, by).await?; provider_name: db
self.settings_repo.set("oidc.issuer_url", &dto.issuer_url, cat, false, by).await?; .get("oidc.provider_name")
self.settings_repo.set("oidc.client_id", &dto.client_id, cat, false, by).await?; .cloned()
.unwrap_or(d.provider_name),
if let Some(ref secret) = dto.client_secret };
&& !secret.is_empty() {
self.settings_repo.set("oidc.client_secret", secret, cat, true, by).await?; // Env vars override DB
} self.apply_env_overrides(&mut config);
if let Some(ref v) = dto.scopes { Ok(config)
self.settings_repo.set("oidc.scopes", v, cat, false, by).await?; }
}
if let Some(v) = dto.auto_provision { /// Get OIDC settings for display in admin UI (secrets masked).
self.settings_repo.set("oidc.auto_provision", &v.to_string(), cat, false, by).await?; pub async fn get_oidc_settings(&self) -> Result<OidcSettingsDto, DomainError> {
} let db = self.settings_repo.get_by_category("oidc").await?;
if let Some(ref v) = dto.admin_groups { let d = OidcConfig::default();
self.settings_repo.set("oidc.admin_groups", v, cat, false, by).await?;
} let has_secret = db
if let Some(v) = dto.disable_password_login { .get("oidc.client_secret")
self.settings_repo.set("oidc.disable_password_login", &v.to_string(), cat, false, by).await?; .map(|s| !s.is_empty())
} .unwrap_or(false)
if let Some(ref v) = dto.provider_name { || std::env::var("OXICLOUD_OIDC_CLIENT_SECRET")
self.settings_repo.set("oidc.provider_name", v, cat, false, by).await?; .map(|s| !s.is_empty())
} .unwrap_or(false);
// Hot-reload OIDC service Ok(OidcSettingsDto {
let eff = self.load_effective_oidc_config().await?; enabled: db
if eff.enabled && !eff.issuer_url.is_empty() .get("oidc.enabled")
&& !eff.client_id.is_empty() && !eff.client_secret.is_empty() .and_then(|v| v.parse().ok())
{ .unwrap_or(d.enabled),
let svc = Arc::new(OidcService::new(eff.clone())); issuer_url: db.get("oidc.issuer_url").cloned().unwrap_or_default(),
self.auth_app_service.reload_oidc(svc, eff); client_id: db.get("oidc.client_id").cloned().unwrap_or_default(),
tracing::info!("OIDC service hot-reloaded with new configuration"); client_secret_set: has_secret,
} else if !eff.enabled { scopes: db.get("oidc.scopes").cloned().unwrap_or(d.scopes),
self.auth_app_service.disable_oidc(); auto_provision: db
tracing::info!("OIDC service disabled via admin panel"); .get("oidc.auto_provision")
} .and_then(|v| v.parse().ok())
.unwrap_or(d.auto_provision),
Ok(()) admin_groups: db.get("oidc.admin_groups").cloned().unwrap_or_default(),
} disable_password_login: db
.get("oidc.disable_password_login")
/// Test OIDC connection by fetching the discovery document. .and_then(|v| v.parse().ok())
pub async fn test_oidc_connection( .unwrap_or(d.disable_password_login),
&self, provider_name: db
dto: TestOidcConnectionDto, .get("oidc.provider_name")
) -> Result<OidcTestResultDto, DomainError> { .cloned()
let issuer = dto.issuer_url.trim_end_matches('/'); .unwrap_or(d.provider_name),
let discovery_url = format!("{}/.well-known/openid-configuration", issuer); callback_url: self.callback_url(),
env_overrides: self.get_env_overrides(),
let client = reqwest::Client::builder() })
.timeout(std::time::Duration::from_secs(10)) }
.build()
.map_err(|e| DomainError::new( /// Save OIDC settings to DB and hot-reload the OIDC service.
ErrorKind::InternalError, "OIDC", format!("HTTP client error: {}", e), pub async fn save_oidc_settings(
))?; &self,
dto: SaveOidcSettingsDto,
let resp = match client.get(&discovery_url).send().await { updated_by: &str,
Ok(r) => r, ) -> Result<(), DomainError> {
Err(e) => { let cat = "oidc";
return Ok(OidcTestResultDto { let by = Some(updated_by);
success: false,
message: format!("Cannot reach the OIDC provider: {}. Check your Issuer URL.", e), self.settings_repo
issuer: None, .set("oidc.enabled", &dto.enabled.to_string(), cat, false, by)
authorization_endpoint: None, .await?;
token_endpoint: None, self.settings_repo
userinfo_endpoint: None, .set("oidc.issuer_url", &dto.issuer_url, cat, false, by)
provider_name_suggestion: None, .await?;
}); self.settings_repo
} .set("oidc.client_id", &dto.client_id, cat, false, by)
}; .await?;
if !resp.status().is_success() { if let Some(ref secret) = dto.client_secret
return Ok(OidcTestResultDto { && !secret.is_empty()
success: false, {
message: format!( self.settings_repo
"OIDC discovery returned HTTP {} — the Issuer URL may be incorrect.", .set("oidc.client_secret", secret, cat, true, by)
resp.status() .await?;
), }
issuer: None, if let Some(ref v) = dto.scopes {
authorization_endpoint: None, self.settings_repo
token_endpoint: None, .set("oidc.scopes", v, cat, false, by)
userinfo_endpoint: None, .await?;
provider_name_suggestion: None, }
}); if let Some(v) = dto.auto_provision {
} self.settings_repo
.set("oidc.auto_provision", &v.to_string(), cat, false, by)
#[derive(serde::Deserialize)] .await?;
struct Discovery { }
issuer: Option<String>, if let Some(ref v) = dto.admin_groups {
authorization_endpoint: Option<String>, self.settings_repo
token_endpoint: Option<String>, .set("oidc.admin_groups", v, cat, false, by)
userinfo_endpoint: Option<String>, .await?;
} }
if let Some(v) = dto.disable_password_login {
let disc: Discovery = match resp.json().await { self.settings_repo
Ok(d) => d, .set(
Err(e) => { "oidc.disable_password_login",
return Ok(OidcTestResultDto { &v.to_string(),
success: false, cat,
message: format!("Invalid discovery document: {}", e), false,
issuer: None, by,
authorization_endpoint: None, )
token_endpoint: None, .await?;
userinfo_endpoint: None, }
provider_name_suggestion: None, if let Some(ref v) = dto.provider_name {
}); self.settings_repo
} .set("oidc.provider_name", v, cat, false, by)
}; .await?;
}
// Suggest provider name from hostname
let suggestion = issuer // Hot-reload OIDC service
.trim_start_matches("https://") let eff = self.load_effective_oidc_config().await?;
.trim_start_matches("http://") if eff.enabled
.split('/') && !eff.issuer_url.is_empty()
.next() && !eff.client_id.is_empty()
.and_then(|host| { && !eff.client_secret.is_empty()
let parts: Vec<&str> = host.split('.').collect(); {
let name = if parts.len() >= 2 { parts[0] } else { host }; let svc = Arc::new(OidcService::new(eff.clone()));
let mut c = name.chars(); self.auth_app_service.reload_oidc(svc, eff);
c.next().map(|f| f.to_uppercase().to_string() + c.as_str()) tracing::info!("OIDC service hot-reloaded with new configuration");
}); } else if !eff.enabled {
self.auth_app_service.disable_oidc();
Ok(OidcTestResultDto { tracing::info!("OIDC service disabled via admin panel");
success: true, }
message: "OIDC provider is reachable and returned a valid discovery document.".into(),
issuer: disc.issuer, Ok(())
authorization_endpoint: disc.authorization_endpoint, }
token_endpoint: disc.token_endpoint,
userinfo_endpoint: disc.userinfo_endpoint, /// Test OIDC connection by fetching the discovery document.
provider_name_suggestion: suggestion, pub async fn test_oidc_connection(
}) &self,
} dto: TestOidcConnectionDto,
) -> Result<OidcTestResultDto, DomainError> {
// ======================================================================== let issuer = dto.issuer_url.trim_end_matches('/');
// Registration Control let discovery_url = format!("{}/.well-known/openid-configuration", issuer);
// ========================================================================
let client = reqwest::Client::builder()
/// Check if public self-registration is enabled. .timeout(std::time::Duration::from_secs(10))
/// Priority: env var `OXICLOUD_DISABLE_REGISTRATION` > DB setting > default (true). .build()
pub async fn get_registration_enabled(&self) -> bool { .map_err(|e| {
// Env var override takes priority DomainError::new(
if let Ok(val) = std::env::var("OXICLOUD_DISABLE_REGISTRATION") { ErrorKind::InternalError,
return !matches!(val.to_lowercase().as_str(), "true" | "1" | "yes"); "OIDC",
} format!("HTTP client error: {}", e),
// Check DB setting )
match self.settings_repo.get("registration_enabled").await { })?;
Ok(Some(val)) => val == "true",
_ => true, // default: enabled let resp = match client.get(&discovery_url).send().await {
} Ok(r) => r,
} Err(e) => {
return Ok(OidcTestResultDto {
/// Enable or disable public self-registration. success: false,
pub async fn set_registration_enabled( message: format!(
&self, "Cannot reach the OIDC provider: {}. Check your Issuer URL.",
enabled: bool, e
updated_by: &str, ),
) -> Result<(), DomainError> { issuer: None,
self.settings_repo.set( authorization_endpoint: None,
"registration_enabled", token_endpoint: None,
if enabled { "true" } else { "false" }, userinfo_endpoint: None,
"general", provider_name_suggestion: None,
false, });
Some(updated_by), }
).await };
}
} if !resp.status().is_success() {
return Ok(OidcTestResultDto {
success: false,
message: format!(
"OIDC discovery returned HTTP {} — the Issuer URL may be incorrect.",
resp.status()
),
issuer: None,
authorization_endpoint: None,
token_endpoint: None,
userinfo_endpoint: None,
provider_name_suggestion: None,
});
}
#[derive(serde::Deserialize)]
struct Discovery {
issuer: Option<String>,
authorization_endpoint: Option<String>,
token_endpoint: Option<String>,
userinfo_endpoint: Option<String>,
}
let disc: Discovery = match resp.json().await {
Ok(d) => d,
Err(e) => {
return Ok(OidcTestResultDto {
success: false,
message: format!("Invalid discovery document: {}", e),
issuer: None,
authorization_endpoint: None,
token_endpoint: None,
userinfo_endpoint: None,
provider_name_suggestion: None,
});
}
};
// Suggest provider name from hostname
let suggestion = issuer
.trim_start_matches("https://")
.trim_start_matches("http://")
.split('/')
.next()
.and_then(|host| {
let parts: Vec<&str> = host.split('.').collect();
let name = if parts.len() >= 2 { parts[0] } else { host };
let mut c = name.chars();
c.next().map(|f| f.to_uppercase().to_string() + c.as_str())
});
Ok(OidcTestResultDto {
success: true,
message: "OIDC provider is reachable and returned a valid discovery document.".into(),
issuer: disc.issuer,
authorization_endpoint: disc.authorization_endpoint,
token_endpoint: disc.token_endpoint,
userinfo_endpoint: disc.userinfo_endpoint,
provider_name_suggestion: suggestion,
})
}
// ========================================================================
// Registration Control
// ========================================================================
/// Check if public self-registration is enabled.
/// Priority: env var `OXICLOUD_DISABLE_REGISTRATION` > DB setting > default (true).
pub async fn get_registration_enabled(&self) -> bool {
// Env var override takes priority
if let Ok(val) = std::env::var("OXICLOUD_DISABLE_REGISTRATION") {
return !matches!(val.to_lowercase().as_str(), "true" | "1" | "yes");
}
// Check DB setting
match self.settings_repo.get("registration_enabled").await {
Ok(Some(val)) => val == "true",
_ => true, // default: enabled
}
}
/// Enable or disable public self-registration.
pub async fn set_registration_enabled(
&self,
enabled: bool,
updated_by: &str,
) -> Result<(), DomainError> {
self.settings_repo
.set(
"registration_enabled",
if enabled { "true" } else { "false" },
"general",
false,
Some(updated_by),
)
.await
}
}
File diff suppressed because it is too large Load Diff
+178 -159
View File
@@ -1,32 +1,32 @@
use futures::{Future, future::join_all};
use std::sync::Arc; use std::sync::Arc;
use futures::{future::join_all, Future}; use thiserror::Error;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use tracing::info; use tracing::info;
use thiserror::Error;
use crate::application::ports::file_ports::{FileRetrievalUseCase, FileManagementUseCase};
use crate::application::services::folder_service::FolderService;
use crate::common::errors::DomainError;
use crate::common::config::AppConfig;
use crate::application::ports::inbound::FolderUseCase;
use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto; use crate::application::dtos::folder_dto::FolderDto;
use crate::application::ports::file_ports::{FileManagementUseCase, FileRetrievalUseCase};
use crate::application::ports::inbound::FolderUseCase;
use crate::application::services::folder_service::FolderService;
use crate::common::config::AppConfig;
use crate::common::errors::DomainError;
/// Specific errors for batch operations /// Specific errors for batch operations
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum BatchOperationError { pub enum BatchOperationError {
#[error("Domain error: {0}")] #[error("Domain error: {0}")]
Domain(#[from] DomainError), Domain(#[from] DomainError),
#[error("Operation cancelled: {0}")] #[error("Operation cancelled: {0}")]
Cancelled(String), Cancelled(String),
#[error("Concurrency limit exceeded: {0}")] #[error("Concurrency limit exceeded: {0}")]
ConcurrencyLimit(String), ConcurrencyLimit(String),
#[error("Batch operation error: {0} ({1} of {2} completed)")] #[error("Batch operation error: {0} ({1} of {2} completed)")]
PartialFailure(String, usize, usize), PartialFailure(String, usize, usize),
#[error("Internal error: {0}")] #[error("Internal error: {0}")]
Internal(String), Internal(String),
} }
@@ -72,11 +72,11 @@ impl BatchOperationService {
file_retrieval: Arc<dyn FileRetrievalUseCase>, file_retrieval: Arc<dyn FileRetrievalUseCase>,
file_management: Arc<dyn FileManagementUseCase>, file_management: Arc<dyn FileManagementUseCase>,
folder_service: Arc<FolderService>, folder_service: Arc<FolderService>,
config: AppConfig config: AppConfig,
) -> Self { ) -> Self {
// Limit concurrency based on configuration // Limit concurrency based on configuration
let max_concurrency = config.concurrency.max_concurrent_files; let max_concurrency = config.concurrency.max_concurrent_files;
Self { Self {
file_retrieval, file_retrieval,
file_management, file_management,
@@ -85,16 +85,21 @@ impl BatchOperationService {
semaphore: Arc::new(Semaphore::new(max_concurrency)), semaphore: Arc::new(Semaphore::new(max_concurrency)),
} }
} }
/// Creates a new instance with default configuration /// Creates a new instance with default configuration
pub fn default( pub fn default(
file_retrieval: Arc<dyn FileRetrievalUseCase>, file_retrieval: Arc<dyn FileRetrievalUseCase>,
file_management: Arc<dyn FileManagementUseCase>, file_management: Arc<dyn FileManagementUseCase>,
folder_service: Arc<FolderService> folder_service: Arc<FolderService>,
) -> Self { ) -> Self {
Self::new(file_retrieval, file_management, folder_service, AppConfig::default()) Self::new(
file_retrieval,
file_management,
folder_service,
AppConfig::default(),
)
} }
/// Copies multiple files in parallel /// Copies multiple files in parallel
pub async fn copy_files( pub async fn copy_files(
&self, &self,
@@ -103,7 +108,7 @@ impl BatchOperationService {
) -> Result<BatchResult<FileDto>, BatchOperationError> { ) -> Result<BatchResult<FileDto>, BatchOperationError> {
info!("Starting batch copy of {} files", file_ids.len()); info!("Starting batch copy of {} files", file_ids.len());
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
// Create result structure // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
@@ -113,30 +118,30 @@ impl BatchOperationService {
..Default::default() ..Default::default()
}, },
}; };
// Define the operation to perform for each file // Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| { let operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone(); let mgmt = self.file_management.clone();
let target_folder = target_folder_id.clone(); let target_folder = target_folder_id.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { async move {
// Acquire semaphore permit // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let copy_result = mgmt.move_file(&file_id, target_folder.clone()).await; let copy_result = mgmt.move_file(&file_id, target_folder.clone()).await;
// Release the permit explicitly (also released on drop) // Release the permit explicitly (also released on drop)
drop(permit); drop(permit);
// Return the result along with the ID to identify successes/failures // Return the result along with the ID to identify successes/failures
(file_id, copy_result) (file_id, copy_result)
} }
}); });
// Execute all operations in parallel with concurrency control // Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await; let operation_results = join_all(operations).await;
// Process the results // Process the results
for (file_id, operation_result) in operation_results { for (file_id, operation_result) in operation_results {
match operation_result { match operation_result {
@@ -150,22 +155,23 @@ impl BatchOperationService {
} }
} }
} }
// Complete statistics // Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self
.config
.concurrency
.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Batch copy completed: {}/{} successful in {}ms", "Batch copy completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful, result.stats.total, result.stats.execution_time_ms
result.stats.total,
result.stats.execution_time_ms
); );
Ok(result) Ok(result)
} }
/// Moves multiple files in parallel /// Moves multiple files in parallel
pub async fn move_files( pub async fn move_files(
&self, &self,
@@ -174,7 +180,7 @@ impl BatchOperationService {
) -> Result<BatchResult<FileDto>, BatchOperationError> { ) -> Result<BatchResult<FileDto>, BatchOperationError> {
info!("Starting batch move of {} files", file_ids.len()); info!("Starting batch move of {} files", file_ids.len());
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
// Create result structure // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
@@ -184,30 +190,30 @@ impl BatchOperationService {
..Default::default() ..Default::default()
}, },
}; };
// Define the operation to perform for each file // Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| { let operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone(); let mgmt = self.file_management.clone();
let target_folder = target_folder_id.clone(); let target_folder = target_folder_id.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { async move {
// Acquire semaphore permit // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let move_result = mgmt.move_file(&file_id, target_folder.clone()).await; let move_result = mgmt.move_file(&file_id, target_folder.clone()).await;
// Release the permit explicitly // Release the permit explicitly
drop(permit); drop(permit);
// Return the result along with the ID to identify successes/failures // Return the result along with the ID to identify successes/failures
(file_id, move_result) (file_id, move_result)
} }
}); });
// Execute all operations in parallel with concurrency control // Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await; let operation_results = join_all(operations).await;
// Process the results // Process the results
for (file_id, operation_result) in operation_results { for (file_id, operation_result) in operation_results {
match operation_result { match operation_result {
@@ -221,22 +227,23 @@ impl BatchOperationService {
} }
} }
} }
// Complete statistics // Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self
.config
.concurrency
.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Batch move completed: {}/{} successful in {}ms", "Batch move completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful, result.stats.total, result.stats.execution_time_ms
result.stats.total,
result.stats.execution_time_ms
); );
Ok(result) Ok(result)
} }
/// Deletes multiple files in parallel /// Deletes multiple files in parallel
pub async fn delete_files( pub async fn delete_files(
&self, &self,
@@ -244,7 +251,7 @@ impl BatchOperationService {
) -> Result<BatchResult<String>, BatchOperationError> { ) -> Result<BatchResult<String>, BatchOperationError> {
info!("Starting batch deletion of {} files", file_ids.len()); info!("Starting batch deletion of {} files", file_ids.len());
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
// Create result structure // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
@@ -254,30 +261,30 @@ impl BatchOperationService {
..Default::default() ..Default::default()
}, },
}; };
// Define the operation to perform for each file // Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| { let operations = file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone(); let mgmt = self.file_management.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
let id_clone = file_id.clone(); let id_clone = file_id.clone();
async move { async move {
// Acquire semaphore permit // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let delete_result = mgmt.delete_file(&file_id).await; let delete_result = mgmt.delete_file(&file_id).await;
// Release the permit explicitly // Release the permit explicitly
drop(permit); drop(permit);
// Return the result along with the ID // Return the result along with the ID
(id_clone.clone(), delete_result.map(|_| id_clone)) (id_clone.clone(), delete_result.map(|_| id_clone))
} }
}); });
// Execute all operations in parallel with concurrency control // Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await; let operation_results = join_all(operations).await;
// Process the results // Process the results
for (file_id, operation_result) in operation_results { for (file_id, operation_result) in operation_results {
match operation_result { match operation_result {
@@ -291,22 +298,23 @@ impl BatchOperationService {
} }
} }
} }
// Complete statistics // Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self
.config
.concurrency
.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Batch deletion completed: {}/{} successful in {}ms", "Batch deletion completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful, result.stats.total, result.stats.execution_time_ms
result.stats.total,
result.stats.execution_time_ms
); );
Ok(result) Ok(result)
} }
/// Loads multiple files in parallel (data in memory) /// Loads multiple files in parallel (data in memory)
pub async fn get_multiple_files( pub async fn get_multiple_files(
&self, &self,
@@ -314,7 +322,7 @@ impl BatchOperationService {
) -> Result<BatchResult<FileDto>, BatchOperationError> { ) -> Result<BatchResult<FileDto>, BatchOperationError> {
info!("Starting batch load of {} files", file_ids.len()); info!("Starting batch load of {} files", file_ids.len());
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
// Create result structure // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
@@ -324,29 +332,29 @@ impl BatchOperationService {
..Default::default() ..Default::default()
}, },
}; };
// Define the operation to perform for each file // Define the operation to perform for each file
let operations = file_ids.into_iter().map(|file_id| { let operations = file_ids.into_iter().map(|file_id| {
let retrieval = self.file_retrieval.clone(); let retrieval = self.file_retrieval.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { async move {
// Acquire semaphore permit // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let get_result = retrieval.get_file(&file_id).await; let get_result = retrieval.get_file(&file_id).await;
// Release the permit explicitly // Release the permit explicitly
drop(permit); drop(permit);
// Return the result along with the ID // Return the result along with the ID
(file_id, get_result) (file_id, get_result)
} }
}); });
// Execute all operations in parallel with concurrency control // Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await; let operation_results = join_all(operations).await;
// Process the results // Process the results
for (file_id, operation_result) in operation_results { for (file_id, operation_result) in operation_results {
match operation_result { match operation_result {
@@ -360,22 +368,23 @@ impl BatchOperationService {
} }
} }
} }
// Complete statistics // Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self
.config
.concurrency
.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Batch load completed: {}/{} successful in {}ms", "Batch load completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful, result.stats.total, result.stats.execution_time_ms
result.stats.total,
result.stats.execution_time_ms
); );
Ok(result) Ok(result)
} }
/// Deletes multiple folders in parallel /// Deletes multiple folders in parallel
pub async fn delete_folders( pub async fn delete_folders(
&self, &self,
@@ -384,7 +393,7 @@ impl BatchOperationService {
) -> Result<BatchResult<String>, BatchOperationError> { ) -> Result<BatchResult<String>, BatchOperationError> {
info!("Starting batch deletion of {} folders", folder_ids.len()); info!("Starting batch deletion of {} folders", folder_ids.len());
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
// Create result structure // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
@@ -394,32 +403,32 @@ impl BatchOperationService {
..Default::default() ..Default::default()
}, },
}; };
// Define the operation to perform for each folder // Define the operation to perform for each folder
let operations = folder_ids.into_iter().map(|folder_id| { let operations = folder_ids.into_iter().map(|folder_id| {
let folder_service = self.folder_service.clone(); let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
let id_clone = folder_id.clone(); let id_clone = folder_id.clone();
async move { async move {
// Acquire semaphore permit // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
// For both recursive and non-recursive, use the standard delete_folder method // For both recursive and non-recursive, use the standard delete_folder method
// since FolderUseCase only has a single delete_folder method // since FolderUseCase only has a single delete_folder method
let delete_result = folder_service.delete_folder(&folder_id).await; let delete_result = folder_service.delete_folder(&folder_id).await;
// Release the permit explicitly // Release the permit explicitly
drop(permit); drop(permit);
// Return the result along with the ID // Return the result along with the ID
(id_clone.clone(), delete_result.map(|_| id_clone)) (id_clone.clone(), delete_result.map(|_| id_clone))
} }
}); });
// Execute all operations in parallel with concurrency control // Execute all operations in parallel with concurrency control
let operation_results = join_all(operations).await; let operation_results = join_all(operations).await;
// Process the results // Process the results
for (folder_id, operation_result) in operation_results { for (folder_id, operation_result) in operation_results {
match operation_result { match operation_result {
@@ -433,22 +442,23 @@ impl BatchOperationService {
} }
} }
} }
// Complete statistics // Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self
.config
.concurrency
.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Batch folder deletion completed: {}/{} successful in {}ms", "Batch folder deletion completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful, result.stats.total, result.stats.execution_time_ms
result.stats.total,
result.stats.execution_time_ms
); );
Ok(result) Ok(result)
} }
/// Generic batch operation for any type of async function /// Generic batch operation for any type of async function
pub async fn generic_batch_operation<T, F, Fut>( pub async fn generic_batch_operation<T, F, Fut>(
&self, &self,
@@ -460,9 +470,12 @@ impl BatchOperationService {
F: Fn(T, Arc<Semaphore>) -> Fut + Clone + Send + Sync + 'static, F: Fn(T, Arc<Semaphore>) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Result<T, DomainError>> + Send + 'static, Fut: Future<Output = Result<T, DomainError>> + Send + 'static,
{ {
info!("Starting generic batch operation with {} items", items.len()); info!(
"Starting generic batch operation with {} items",
items.len()
);
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
// Create result structure // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
@@ -472,25 +485,25 @@ impl BatchOperationService {
..Default::default() ..Default::default()
}, },
}; };
// Convert each item to a task // Convert each item to a task
let tasks = items.iter().map(|item| { let tasks = items.iter().map(|item| {
let item_clone = item.clone(); let item_clone = item.clone();
let op = operation.clone(); let op = operation.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { async move {
// The provided function must handle semaphore acquisition // The provided function must handle semaphore acquisition
let op_result = op(item_clone.clone(), semaphore).await; let op_result = op(item_clone.clone(), semaphore).await;
// Return the result along with the original item for identification // Return the result along with the original item for identification
(item_clone, op_result) (item_clone, op_result)
} }
}); });
// Execute all tasks in parallel // Execute all tasks in parallel
let operation_results = join_all(tasks).await; let operation_results = join_all(tasks).await;
// Process results // Process results
for (item, operation_result) in operation_results { for (item, operation_result) in operation_results {
match operation_result { match operation_result {
@@ -505,22 +518,23 @@ impl BatchOperationService {
} }
} }
} }
// Complete statistics // Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self
.config
.concurrency
.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Generic batch operation completed: {}/{} successful in {}ms", "Generic batch operation completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful, result.stats.total, result.stats.execution_time_ms
result.stats.total,
result.stats.execution_time_ms
); );
Ok(result) Ok(result)
} }
/// Create multiple folders in parallel /// Create multiple folders in parallel
pub async fn create_folders( pub async fn create_folders(
&self, &self,
@@ -528,7 +542,7 @@ impl BatchOperationService {
) -> Result<BatchResult<FolderDto>, BatchOperationError> { ) -> Result<BatchResult<FolderDto>, BatchOperationError> {
info!("Starting batch creation of {} folders", folders.len()); info!("Starting batch creation of {} folders", folders.len());
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
// Create result structure // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
@@ -538,34 +552,34 @@ impl BatchOperationService {
..Default::default() ..Default::default()
}, },
}; };
// Define the operation for each folder // Define the operation for each folder
let operations = folders.into_iter().map(|(name, parent_id)| { let operations = folders.into_iter().map(|(name, parent_id)| {
let folder_service = self.folder_service.clone(); let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { async move {
// Acquire semaphore permit // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let dto = crate::application::dtos::folder_dto::CreateFolderDto { let dto = crate::application::dtos::folder_dto::CreateFolderDto {
name: name.clone(), name: name.clone(),
parent_id: parent_id.clone() parent_id: parent_id.clone(),
}; };
let create_result = folder_service.create_folder(dto).await; let create_result = folder_service.create_folder(dto).await;
// Release the permit explicitly // Release the permit explicitly
drop(permit); drop(permit);
// Return the result with an identifier for errors // Return the result with an identifier for errors
let id = format!("{}:{}", name, parent_id.unwrap_or_default()); let id = format!("{}:{}", name, parent_id.unwrap_or_default());
(id, create_result) (id, create_result)
} }
}); });
// Execute all operations in parallel // Execute all operations in parallel
let operation_results = join_all(operations).await; let operation_results = join_all(operations).await;
// Process the results // Process the results
for (id, operation_result) in operation_results { for (id, operation_result) in operation_results {
match operation_result { match operation_result {
@@ -579,22 +593,23 @@ impl BatchOperationService {
} }
} }
} }
// Complete statistics // Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self
.config
.concurrency
.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Batch folder creation completed: {}/{} successful in {}ms", "Batch folder creation completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful, result.stats.total, result.stats.execution_time_ms
result.stats.total,
result.stats.execution_time_ms
); );
Ok(result) Ok(result)
} }
/// Get metadata of multiple folders in parallel /// Get metadata of multiple folders in parallel
pub async fn get_multiple_folders( pub async fn get_multiple_folders(
&self, &self,
@@ -602,7 +617,7 @@ impl BatchOperationService {
) -> Result<BatchResult<FolderDto>, BatchOperationError> { ) -> Result<BatchResult<FolderDto>, BatchOperationError> {
info!("Starting batch load of {} folders", folder_ids.len()); info!("Starting batch load of {} folders", folder_ids.len());
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
// Create result structure // Create result structure
let mut result = BatchResult { let mut result = BatchResult {
successful: Vec::new(), successful: Vec::new(),
@@ -612,29 +627,29 @@ impl BatchOperationService {
..Default::default() ..Default::default()
}, },
}; };
// Define the operation for each folder // Define the operation for each folder
let operations = folder_ids.into_iter().map(|folder_id| { let operations = folder_ids.into_iter().map(|folder_id| {
let folder_service = self.folder_service.clone(); let folder_service = self.folder_service.clone();
let semaphore = self.semaphore.clone(); let semaphore = self.semaphore.clone();
async move { async move {
// Acquire semaphore permit // Acquire semaphore permit
let permit = semaphore.acquire().await.unwrap(); let permit = semaphore.acquire().await.unwrap();
let get_result = folder_service.get_folder(&folder_id).await; let get_result = folder_service.get_folder(&folder_id).await;
// Release the permit explicitly // Release the permit explicitly
drop(permit); drop(permit);
// Return the result with its ID // Return the result with its ID
(folder_id, get_result) (folder_id, get_result)
} }
}); });
// Execute all operations in parallel // Execute all operations in parallel
let operation_results = join_all(operations).await; let operation_results = join_all(operations).await;
// Process the results // Process the results
for (folder_id, operation_result) in operation_results { for (folder_id, operation_result) in operation_results {
match operation_result { match operation_result {
@@ -648,19 +663,20 @@ impl BatchOperationService {
} }
} }
} }
// Complete statistics // Complete statistics
result.stats.execution_time_ms = start_time.elapsed().as_millis(); result.stats.execution_time_ms = start_time.elapsed().as_millis();
result.stats.max_concurrency = self.config.concurrency.max_concurrent_files result.stats.max_concurrency = self
.config
.concurrency
.max_concurrent_files
.min(result.stats.total); .min(result.stats.total);
info!( info!(
"Batch folder load completed: {}/{} successful in {}ms", "Batch folder load completed: {}/{} successful in {}ms",
result.stats.successful, result.stats.successful, result.stats.total, result.stats.execution_time_ms
result.stats.total,
result.stats.execution_time_ms
); );
Ok(result) Ok(result)
} }
} }
@@ -668,26 +684,26 @@ impl BatchOperationService {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::common::stubs::{StubFileManagementUseCase, StubFileRetrievalUseCase};
use std::sync::Arc; use std::sync::Arc;
use crate::common::stubs::{StubFileRetrievalUseCase, StubFileManagementUseCase};
#[tokio::test] #[tokio::test]
async fn test_generic_batch_operation() { async fn test_generic_batch_operation() {
// Create the batch service with stubs // Create the batch service with stubs
let batch_service = BatchOperationService::new( let batch_service = BatchOperationService::new(
Arc::new(StubFileRetrievalUseCase), Arc::new(StubFileRetrievalUseCase),
Arc::new(StubFileManagementUseCase), Arc::new(StubFileManagementUseCase),
Arc::new(FolderService::new( Arc::new(FolderService::new(Arc::new(
Arc::new(crate::common::stubs::StubFolderStoragePort) crate::common::stubs::StubFolderStoragePort,
)), ))),
AppConfig::default() AppConfig::default(),
); );
// Define a generic test operation // Define a generic test operation
let operation = |item: i32, semaphore: Arc<Semaphore>| async move { let operation = |item: i32, semaphore: Arc<Semaphore>| async move {
// Acquire and release the semaphore // Acquire and release the semaphore
let _permit = semaphore.acquire().await.unwrap(); let _permit = semaphore.acquire().await.unwrap();
if item % 2 == 0 { if item % 2 == 0 {
// Simulate success for even numbers // Simulate success for even numbers
Ok(item * 2) Ok(item * 2)
@@ -696,22 +712,25 @@ mod tests {
Err(DomainError::validation_error("Odd number not allowed")) Err(DomainError::validation_error("Odd number not allowed"))
} }
}; };
// Execute the batch operation // Execute the batch operation
let items = vec![1, 2, 3, 4, 5]; let items = vec![1, 2, 3, 4, 5];
let result = batch_service.generic_batch_operation(items, operation).await.unwrap(); let result = batch_service
.generic_batch_operation(items, operation)
.await
.unwrap();
// Verify the results // Verify the results
assert_eq!(result.stats.total, 5); assert_eq!(result.stats.total, 5);
assert_eq!(result.stats.successful, 2); assert_eq!(result.stats.successful, 2);
assert_eq!(result.stats.failed, 3); assert_eq!(result.stats.failed, 3);
// Even numbers should be in successes, doubled // Even numbers should be in successes, doubled
assert!(result.successful.contains(&4)); // 2*2 assert!(result.successful.contains(&4)); // 2*2
assert!(result.successful.contains(&8)); // 4*2 assert!(result.successful.contains(&8)); // 4*2
// Odd numbers should be in failures // Odd numbers should be in failures
assert_eq!(result.failed.len(), 3); assert_eq!(result.failed.len(), 3);
} }
} }
+371 -183
View File
@@ -1,10 +1,10 @@
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use std::sync::Arc;
use crate::application::dtos::calendar_dto::{ use crate::application::dtos::calendar_dto::{
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto, CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto,
CreateEventDto, UpdateEventDto, CreateEventICalDto UpdateCalendarDto, UpdateEventDto,
}; };
use crate::application::ports::calendar_ports::{CalendarStoragePort, CalendarUseCase}; use crate::application::ports::calendar_ports::{CalendarStoragePort, CalendarUseCase};
use crate::common::errors::{DomainError, ErrorKind}; use crate::common::errors::{DomainError, ErrorKind};
@@ -15,389 +15,577 @@ pub struct CalendarService {
impl CalendarService { impl CalendarService {
pub fn new(calendar_storage: Arc<dyn CalendarStoragePort>) -> Self { pub fn new(calendar_storage: Arc<dyn CalendarStoragePort>) -> Self {
Self { Self { calendar_storage }
calendar_storage,
}
} }
} }
#[async_trait] #[async_trait]
impl CalendarUseCase for CalendarService { impl CalendarUseCase for CalendarService {
async fn create_calendar(&self, calendar: CreateCalendarDto) -> Result<CalendarDto, DomainError> { async fn create_calendar(
&self,
calendar: CreateCalendarDto,
) -> Result<CalendarDto, DomainError> {
// This function requires the current user context which will come from middleware // This function requires the current user context which will come from middleware
// For now, we'll use a dummy implementation that needs to be completed // For now, we'll use a dummy implementation that needs to be completed
// In a real implementation, get user_id from current user context // In a real implementation, get user_id from current user context
let user_id = "current_user_id"; // This should come from middleware let user_id = "current_user_id"; // This should come from middleware
self.calendar_storage.create_calendar(calendar, user_id).await self.calendar_storage
.create_calendar(calendar, user_id)
.await
} }
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError> { async fn update_calendar(
&self,
calendar_id: &str,
update: UpdateCalendarDto,
) -> Result<CalendarDto, DomainError> {
// In a real implementation, we would: // In a real implementation, we would:
// 1. Get the current user ID from middleware // 1. Get the current user ID from middleware
// 2. Verify that the user has access to this calendar // 2. Verify that the user has access to this calendar
// 3. Update the calendar if they have permission // 3. Update the calendar if they have permission
let user_id = "current_user_id"; // This should come from middleware let user_id = "current_user_id"; // This should come from middleware
// Check if user has access // Check if user has access
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(calendar_id, user_id)
.await?;
if !has_access { if !has_access {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"You don't have permission to update this calendar" "You don't have permission to update this calendar",
)); ));
} }
self.calendar_storage.update_calendar(calendar_id, update).await self.calendar_storage
.update_calendar(calendar_id, update)
.await
} }
async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError> { async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError> {
let user_id = "current_user_id"; // This should come from middleware let user_id = "current_user_id"; // This should come from middleware
// Check if user has access // Check if user has access
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(calendar_id, user_id)
.await?;
if !has_access { if !has_access {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"You don't have permission to delete this calendar" "You don't have permission to delete this calendar",
)); ));
} }
self.calendar_storage.delete_calendar(calendar_id).await self.calendar_storage.delete_calendar(calendar_id).await
} }
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError> { async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError> {
let user_id = "current_user_id"; // This should come from middleware let user_id = "current_user_id"; // This should come from middleware
// Get the calendar // Get the calendar
let calendar = self.calendar_storage.get_calendar(calendar_id).await?; let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
// Check if user has access or if calendar is public // Check if user has access or if calendar is public
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(calendar_id, user_id)
.await?;
if !has_access && !calendar.is_public { if !has_access && !calendar.is_public {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"You don't have permission to view this calendar" "You don't have permission to view this calendar",
)); ));
} }
Ok(calendar) Ok(calendar)
} }
async fn list_my_calendars(&self) -> Result<Vec<CalendarDto>, DomainError> { async fn list_my_calendars(&self) -> Result<Vec<CalendarDto>, DomainError> {
let user_id = "current_user_id"; // This should come from middleware let user_id = "current_user_id"; // This should come from middleware
self.calendar_storage.list_calendars_by_owner(user_id).await self.calendar_storage.list_calendars_by_owner(user_id).await
} }
async fn list_shared_calendars(&self) -> Result<Vec<CalendarDto>, DomainError> { async fn list_shared_calendars(&self) -> Result<Vec<CalendarDto>, DomainError> {
let user_id = "current_user_id"; // This should come from middleware let user_id = "current_user_id"; // This should come from middleware
self.calendar_storage.list_calendars_shared_with_user(user_id).await self.calendar_storage
.list_calendars_shared_with_user(user_id)
.await
} }
async fn list_public_calendars(&self, limit: Option<i64>, offset: Option<i64>) -> Result<Vec<CalendarDto>, DomainError> { async fn list_public_calendars(
&self,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<CalendarDto>, DomainError> {
let limit = limit.unwrap_or(100); let limit = limit.unwrap_or(100);
let offset = offset.unwrap_or(0); let offset = offset.unwrap_or(0);
self.calendar_storage.list_public_calendars(limit, offset).await self.calendar_storage
.list_public_calendars(limit, offset)
.await
} }
async fn share_calendar(&self, calendar_id: &str, user_id: &str, access_level: &str) -> Result<(), DomainError> { async fn share_calendar(
let current_user_id = "current_user_id"; // This should come from middleware &self,
calendar_id: &str,
user_id: &str,
access_level: &str,
) -> Result<(), DomainError> {
let current_user_id = "current_user_id"; // This should come from middleware
// Check if current user has access // Check if current user has access
let calendar = self.calendar_storage.get_calendar(calendar_id).await?; let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
// Only the owner can share the calendar // Only the owner can share the calendar
if calendar.owner_id != current_user_id { if calendar.owner_id != current_user_id {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"Only the calendar owner can change sharing settings" "Only the calendar owner can change sharing settings",
)); ));
} }
// Validate access_level // Validate access_level
match access_level { match access_level {
"read" | "write" | "owner" => {}, "read" | "write" | "owner" => {}
_ => return Err(DomainError::new( _ => {
ErrorKind::InvalidInput, return Err(DomainError::new(
"Calendar", ErrorKind::InvalidInput,
format!("Invalid access level: {}. Valid values are: read, write, owner", access_level) "Calendar",
)), format!(
"Invalid access level: {}. Valid values are: read, write, owner",
access_level
),
));
}
} }
self.calendar_storage.share_calendar(calendar_id, user_id, access_level).await self.calendar_storage
.share_calendar(calendar_id, user_id, access_level)
.await
} }
async fn remove_calendar_sharing(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError> { async fn remove_calendar_sharing(
let current_user_id = "current_user_id"; // This should come from middleware &self,
calendar_id: &str,
user_id: &str,
) -> Result<(), DomainError> {
let current_user_id = "current_user_id"; // This should come from middleware
// Check if current user has access // Check if current user has access
let calendar = self.calendar_storage.get_calendar(calendar_id).await?; let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
// Only the owner can change sharing settings // Only the owner can change sharing settings
if calendar.owner_id != current_user_id { if calendar.owner_id != current_user_id {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"Only the calendar owner can change sharing settings" "Only the calendar owner can change sharing settings",
)); ));
} }
self.calendar_storage.remove_calendar_sharing(calendar_id, user_id).await self.calendar_storage
.remove_calendar_sharing(calendar_id, user_id)
.await
} }
async fn get_calendar_shares(&self, calendar_id: &str) -> Result<Vec<(String, String)>, DomainError> { async fn get_calendar_shares(
let current_user_id = "current_user_id"; // This should come from middleware &self,
calendar_id: &str,
) -> Result<Vec<(String, String)>, DomainError> {
let current_user_id = "current_user_id"; // This should come from middleware
// Check if current user has access // Check if current user has access
let calendar = self.calendar_storage.get_calendar(calendar_id).await?; let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
// Only the owner can view sharing settings // Only the owner can view sharing settings
if calendar.owner_id != current_user_id { if calendar.owner_id != current_user_id {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"Only the calendar owner can view sharing settings" "Only the calendar owner can view sharing settings",
)); ));
} }
self.calendar_storage.get_calendar_shares(calendar_id).await self.calendar_storage.get_calendar_shares(calendar_id).await
} }
async fn create_event(&self, event: CreateEventDto) -> Result<CalendarEventDto, DomainError> { async fn create_event(&self, event: CreateEventDto) -> Result<CalendarEventDto, DomainError> {
let user_id = "current_user_id"; // This should come from middleware let user_id = "current_user_id"; // This should come from middleware
// Check if user has access to the calendar // Check if user has access to the calendar
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(&event.calendar_id, user_id)
.await?;
if !has_access { if !has_access {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"You don't have permission to add events to this calendar" "You don't have permission to add events to this calendar",
)); ));
} }
self.calendar_storage.create_event(event).await self.calendar_storage.create_event(event).await
} }
async fn create_event_from_ical(&self, event: CreateEventICalDto) -> Result<CalendarEventDto, DomainError> { async fn create_event_from_ical(
let user_id = "current_user_id"; // This should come from middleware &self,
event: CreateEventICalDto,
) -> Result<CalendarEventDto, DomainError> {
let user_id = "current_user_id"; // This should come from middleware
// Check if user has access to the calendar // Check if user has access to the calendar
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(&event.calendar_id, user_id)
.await?;
if !has_access { if !has_access {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"You don't have permission to add events to this calendar" "You don't have permission to add events to this calendar",
)); ));
} }
self.calendar_storage.create_event_from_ical(event).await self.calendar_storage.create_event_from_ical(event).await
} }
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError> { async fn update_event(
let user_id = "current_user_id"; // This should come from middleware &self,
event_id: &str,
update: UpdateEventDto,
) -> Result<CalendarEventDto, DomainError> {
let user_id = "current_user_id"; // This should come from middleware
// Get the event to find its calendar // Get the event to find its calendar
let event = self.calendar_storage.get_event(event_id).await?; let event = self.calendar_storage.get_event(event_id).await?;
// Check if user has access to the calendar // Check if user has access to the calendar
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(&event.calendar_id, user_id)
.await?;
if !has_access { if !has_access {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"You don't have permission to update events in this calendar" "You don't have permission to update events in this calendar",
)); ));
} }
self.calendar_storage.update_event(event_id, update).await self.calendar_storage.update_event(event_id, update).await
} }
async fn delete_event(&self, event_id: &str) -> Result<(), DomainError> { async fn delete_event(&self, event_id: &str) -> Result<(), DomainError> {
let user_id = "current_user_id"; // This should come from middleware let user_id = "current_user_id"; // This should come from middleware
// Get the event to find its calendar // Get the event to find its calendar
let event = self.calendar_storage.get_event(event_id).await?; let event = self.calendar_storage.get_event(event_id).await?;
// Check if user has access to the calendar // Check if user has access to the calendar
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(&event.calendar_id, user_id)
.await?;
if !has_access { if !has_access {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"You don't have permission to delete events in this calendar" "You don't have permission to delete events in this calendar",
)); ));
} }
self.calendar_storage.delete_event(event_id).await self.calendar_storage.delete_event(event_id).await
} }
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError> { async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError> {
let user_id = "current_user_id"; // This should come from middleware let user_id = "current_user_id"; // This should come from middleware
// Get the event // Get the event
let event = self.calendar_storage.get_event(event_id).await?; let event = self.calendar_storage.get_event(event_id).await?;
// Check if user has access to the calendar // Check if user has access to the calendar
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(&event.calendar_id, user_id)
.await?;
// Check if calendar is public // Check if calendar is public
let calendar = self.calendar_storage.get_calendar(&event.calendar_id).await?; let calendar = self
.calendar_storage
.get_calendar(&event.calendar_id)
.await?;
if !has_access && !calendar.is_public { if !has_access && !calendar.is_public {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"You don't have permission to view events in this calendar" "You don't have permission to view events in this calendar",
)); ));
} }
Ok(event) Ok(event)
} }
async fn list_events(&self, calendar_id: &str, limit: Option<i64>, offset: Option<i64>) -> Result<Vec<CalendarEventDto>, DomainError> { async fn list_events(
let user_id = "current_user_id"; // This should come from middleware &self,
calendar_id: &str,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<CalendarEventDto>, DomainError> {
let user_id = "current_user_id"; // This should come from middleware
// Check if user has access to the calendar // Check if user has access to the calendar
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(calendar_id, user_id)
.await?;
// Check if calendar is public // Check if calendar is public
let calendar = self.calendar_storage.get_calendar(calendar_id).await?; let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
if !has_access && !calendar.is_public { if !has_access && !calendar.is_public {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"You don't have permission to view events in this calendar" "You don't have permission to view events in this calendar",
)); ));
} }
// Use pagination if provided // Use pagination if provided
if limit.is_some() || offset.is_some() { if limit.is_some() || offset.is_some() {
let limit = limit.unwrap_or(100); let limit = limit.unwrap_or(100);
let offset = offset.unwrap_or(0); let offset = offset.unwrap_or(0);
self.calendar_storage.list_events_by_calendar_paginated(calendar_id, limit, offset).await self.calendar_storage
.list_events_by_calendar_paginated(calendar_id, limit, offset)
.await
} else { } else {
self.calendar_storage.list_events_by_calendar(calendar_id).await self.calendar_storage
.list_events_by_calendar(calendar_id)
.await
} }
} }
async fn get_events_in_range( async fn get_events_in_range(
&self, &self,
calendar_id: &str, calendar_id: &str,
start: DateTime<Utc>, start: DateTime<Utc>,
end: DateTime<Utc> end: DateTime<Utc>,
) -> Result<Vec<CalendarEventDto>, DomainError> { ) -> Result<Vec<CalendarEventDto>, DomainError> {
let user_id = "current_user_id"; // This should come from middleware let user_id = "current_user_id"; // This should come from middleware
// Check if user has access to the calendar // Check if user has access to the calendar
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(calendar_id, user_id)
.await?;
// Check if calendar is public // Check if calendar is public
let calendar = self.calendar_storage.get_calendar(calendar_id).await?; let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
if !has_access && !calendar.is_public { if !has_access && !calendar.is_public {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::AccessDenied, ErrorKind::AccessDenied,
"Calendar", "Calendar",
"You don't have permission to view events in this calendar" "You don't have permission to view events in this calendar",
)); ));
} }
self.calendar_storage.get_events_in_time_range(calendar_id, &start, &end).await self.calendar_storage
.get_events_in_time_range(calendar_id, &start, &end)
.await
} }
// ─── User-contextualized variants (for CalDAV protocol handler) ── // ─── User-contextualized variants (for CalDAV protocol handler) ──
async fn create_calendar_for_user(&self, calendar: CreateCalendarDto, user_id: &str) -> Result<CalendarDto, DomainError> { async fn create_calendar_for_user(
self.calendar_storage.create_calendar(calendar, user_id).await &self,
calendar: CreateCalendarDto,
user_id: &str,
) -> Result<CalendarDto, DomainError> {
self.calendar_storage
.create_calendar(calendar, user_id)
.await
} }
async fn update_calendar_for_user(&self, calendar_id: &str, update: UpdateCalendarDto, user_id: &str) -> Result<CalendarDto, DomainError> { async fn update_calendar_for_user(
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?; &self,
calendar_id: &str,
update: UpdateCalendarDto,
user_id: &str,
) -> Result<CalendarDto, DomainError> {
let has_access = self
.calendar_storage
.check_calendar_access(calendar_id, user_id)
.await?;
if !has_access { if !has_access {
return Err(DomainError::new(ErrorKind::AccessDenied, "Calendar", "You don't have permission to update this calendar")); return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"You don't have permission to update this calendar",
));
} }
self.calendar_storage.update_calendar(calendar_id, update).await self.calendar_storage
.update_calendar(calendar_id, update)
.await
} }
async fn delete_calendar_for_user(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError> { async fn delete_calendar_for_user(
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?; &self,
calendar_id: &str,
user_id: &str,
) -> Result<(), DomainError> {
let has_access = self
.calendar_storage
.check_calendar_access(calendar_id, user_id)
.await?;
if !has_access { if !has_access {
return Err(DomainError::new(ErrorKind::AccessDenied, "Calendar", "You don't have permission to delete this calendar")); return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"You don't have permission to delete this calendar",
));
} }
self.calendar_storage.delete_calendar(calendar_id).await self.calendar_storage.delete_calendar(calendar_id).await
} }
async fn get_calendar_for_user(&self, calendar_id: &str, user_id: &str) -> Result<CalendarDto, DomainError> { async fn get_calendar_for_user(
&self,
calendar_id: &str,
user_id: &str,
) -> Result<CalendarDto, DomainError> {
let calendar = self.calendar_storage.get_calendar(calendar_id).await?; let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(calendar_id, user_id)
.await?;
if !has_access && !calendar.is_public { if !has_access && !calendar.is_public {
return Err(DomainError::new(ErrorKind::AccessDenied, "Calendar", "You don't have permission to view this calendar")); return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"You don't have permission to view this calendar",
));
} }
Ok(calendar) Ok(calendar)
} }
async fn list_my_calendars_for_user(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError> { async fn list_my_calendars_for_user(
&self,
user_id: &str,
) -> Result<Vec<CalendarDto>, DomainError> {
self.calendar_storage.list_calendars_by_owner(user_id).await self.calendar_storage.list_calendars_by_owner(user_id).await
} }
async fn list_events_for_user(&self, calendar_id: &str, limit: Option<i64>, offset: Option<i64>, user_id: &str) -> Result<Vec<CalendarEventDto>, DomainError> { async fn list_events_for_user(
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?; &self,
calendar_id: &str,
limit: Option<i64>,
offset: Option<i64>,
user_id: &str,
) -> Result<Vec<CalendarEventDto>, DomainError> {
let has_access = self
.calendar_storage
.check_calendar_access(calendar_id, user_id)
.await?;
let calendar = self.calendar_storage.get_calendar(calendar_id).await?; let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
if !has_access && !calendar.is_public { if !has_access && !calendar.is_public {
return Err(DomainError::new(ErrorKind::AccessDenied, "Calendar", "You don't have permission to view events in this calendar")); return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"You don't have permission to view events in this calendar",
));
} }
if limit.is_some() || offset.is_some() { if limit.is_some() || offset.is_some() {
let limit = limit.unwrap_or(100); let limit = limit.unwrap_or(100);
let offset = offset.unwrap_or(0); let offset = offset.unwrap_or(0);
self.calendar_storage.list_events_by_calendar_paginated(calendar_id, limit, offset).await self.calendar_storage
.list_events_by_calendar_paginated(calendar_id, limit, offset)
.await
} else { } else {
self.calendar_storage.list_events_by_calendar(calendar_id).await self.calendar_storage
.list_events_by_calendar(calendar_id)
.await
} }
} }
async fn get_events_in_range_for_user(&self, calendar_id: &str, start: DateTime<Utc>, end: DateTime<Utc>, user_id: &str) -> Result<Vec<CalendarEventDto>, DomainError> { async fn get_events_in_range_for_user(
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?; &self,
calendar_id: &str,
start: DateTime<Utc>,
end: DateTime<Utc>,
user_id: &str,
) -> Result<Vec<CalendarEventDto>, DomainError> {
let has_access = self
.calendar_storage
.check_calendar_access(calendar_id, user_id)
.await?;
let calendar = self.calendar_storage.get_calendar(calendar_id).await?; let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
if !has_access && !calendar.is_public { if !has_access && !calendar.is_public {
return Err(DomainError::new(ErrorKind::AccessDenied, "Calendar", "You don't have permission to view events in this calendar")); return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"You don't have permission to view events in this calendar",
));
} }
self.calendar_storage.get_events_in_time_range(calendar_id, &start, &end).await self.calendar_storage
.get_events_in_time_range(calendar_id, &start, &end)
.await
} }
async fn create_event_from_ical_for_user(&self, event: CreateEventICalDto, user_id: &str) -> Result<CalendarEventDto, DomainError> { async fn create_event_from_ical_for_user(
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?; &self,
event: CreateEventICalDto,
user_id: &str,
) -> Result<CalendarEventDto, DomainError> {
let has_access = self
.calendar_storage
.check_calendar_access(&event.calendar_id, user_id)
.await?;
if !has_access { if !has_access {
return Err(DomainError::new(ErrorKind::AccessDenied, "Calendar", "You don't have permission to add events to this calendar")); return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"You don't have permission to add events to this calendar",
));
} }
self.calendar_storage.create_event_from_ical(event).await self.calendar_storage.create_event_from_ical(event).await
} }
async fn delete_event_for_user(&self, event_id: &str, user_id: &str) -> Result<(), DomainError> { async fn delete_event_for_user(
&self,
event_id: &str,
user_id: &str,
) -> Result<(), DomainError> {
let event = self.calendar_storage.get_event(event_id).await?; let event = self.calendar_storage.get_event(event_id).await?;
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?; let has_access = self
.calendar_storage
.check_calendar_access(&event.calendar_id, user_id)
.await?;
if !has_access { if !has_access {
return Err(DomainError::new(ErrorKind::AccessDenied, "Calendar", "You don't have permission to delete events in this calendar")); return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"You don't have permission to delete events in this calendar",
));
} }
self.calendar_storage.delete_event(event_id).await self.calendar_storage.delete_event(event_id).await
} }
} }
File diff suppressed because it is too large Load Diff
+45 -15
View File
@@ -1,9 +1,9 @@
use std::sync::Arc;
use async_trait::async_trait;
use tracing::info;
use crate::common::errors::{Result, DomainError, ErrorKind};
use crate::application::ports::favorites_ports::{FavoritesUseCase, FavoritesRepositoryPort};
use crate::application::dtos::favorites_dto::FavoriteItemDto; use crate::application::dtos::favorites_dto::FavoriteItemDto;
use crate::application::ports::favorites_ports::{FavoritesRepositoryPort, FavoritesUseCase};
use crate::common::errors::{DomainError, ErrorKind, Result};
use async_trait::async_trait;
use std::sync::Arc;
use tracing::info;
/// Implementation of the FavoritesUseCase for managing user favorites. /// Implementation of the FavoritesUseCase for managing user favorites.
/// ///
@@ -26,13 +26,20 @@ impl FavoritesUseCase for FavoritesService {
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>> { async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>> {
info!("Getting favorites for user: {}", user_id); info!("Getting favorites for user: {}", user_id);
let favorites = self.repo.get_favorites(user_id).await?; let favorites = self.repo.get_favorites(user_id).await?;
info!("Retrieved {} favorites for user {}", favorites.len(), user_id); info!(
"Retrieved {} favorites for user {}",
favorites.len(),
user_id
);
Ok(favorites) Ok(favorites)
} }
/// Add an item to user's favorites /// Add an item to user's favorites
async fn add_to_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> { async fn add_to_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
info!("Adding {} '{}' to favorites for user {}", item_type, item_id, user_id); info!(
"Adding {} '{}' to favorites for user {}",
item_type, item_id, user_id
);
if item_type != "file" && item_type != "folder" { if item_type != "file" && item_type != "folder" {
return Err(DomainError::new( return Err(DomainError::new(
@@ -43,25 +50,48 @@ impl FavoritesUseCase for FavoritesService {
} }
self.repo.add_favorite(user_id, item_id, item_type).await?; self.repo.add_favorite(user_id, item_id, item_type).await?;
info!("Successfully added {} '{}' to favorites for user {}", item_type, item_id, user_id); info!(
"Successfully added {} '{}' to favorites for user {}",
item_type, item_id, user_id
);
Ok(()) Ok(())
} }
/// Remove an item from user's favorites /// Remove an item from user's favorites
async fn remove_from_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> { async fn remove_from_favorites(
info!("Removing {} '{}' from favorites for user {}", item_type, item_id, user_id); &self,
let removed = self.repo.remove_favorite(user_id, item_id, item_type).await?; user_id: &str,
item_id: &str,
item_type: &str,
) -> Result<bool> {
info!(
"Removing {} '{}' from favorites for user {}",
item_type, item_id, user_id
);
let removed = self
.repo
.remove_favorite(user_id, item_id, item_type)
.await?;
info!( info!(
"{} {} '{}' from favorites for user {}", "{} {} '{}' from favorites for user {}",
if removed { "Successfully removed" } else { "Did not find" }, if removed {
item_type, item_id, user_id "Successfully removed"
} else {
"Did not find"
},
item_type,
item_id,
user_id
); );
Ok(removed) Ok(removed)
} }
/// Check if an item is in user's favorites /// Check if an item is in user's favorites
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> { async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
info!("Checking if {} '{}' is favorite for user {}", item_type, item_id, user_id); info!(
"Checking if {} '{}' is favorite for user {}",
item_type, item_id, user_id
);
self.repo.is_favorite(user_id, item_id, item_type).await self.repo.is_favorite(user_id, item_id, item_type).await
} }
} }
@@ -1,13 +1,13 @@
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use std::sync::Arc;
use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::file_dto::FileDto;
use crate::application::ports::file_ports::FileManagementUseCase;
use crate::application::ports::storage_ports::{FileWritePort, FileReadPort};
use crate::application::ports::dedup_ports::DedupPort; use crate::application::ports::dedup_ports::DedupPort;
use crate::application::ports::file_ports::FileManagementUseCase;
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::application::ports::trash_ports::TrashUseCase; use crate::application::ports::trash_ports::TrashUseCase;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use tracing::{debug, info, warn, error}; use tracing::{debug, error, info, warn};
/// Service for file management operations (move, delete). /// Service for file management operations (move, delete).
/// ///
@@ -76,9 +76,14 @@ impl FileManagementService {
/// Decrement dedup reference count; log result. /// Decrement dedup reference count; log result.
async fn decrement_dedup_ref(&self, hash: &str) { async fn decrement_dedup_ref(&self, hash: &str) {
let Some(dedup) = &self.dedup_service else { return }; let Some(dedup) = &self.dedup_service else {
return;
};
match dedup.remove_reference(hash).await { match dedup.remove_reference(hash).await {
Ok(true) => info!("🗑️ DEDUP: Blob {} deleted (no more references)", &hash[..12]), Ok(true) => info!(
"🗑️ DEDUP: Blob {} deleted (no more references)",
&hash[..12]
),
Ok(false) => debug!("🔗 DEDUP: Reference removed from blob {}", &hash[..12]), Ok(false) => debug!("🔗 DEDUP: Reference removed from blob {}", &hash[..12]),
Err(e) => warn!("⚠️ DEDUP: Failed to decrement reference: {}", e), Err(e) => warn!("⚠️ DEDUP: Failed to decrement reference: {}", e),
} }
@@ -92,12 +97,19 @@ impl FileManagementUseCase for FileManagementService {
file_id: &str, file_id: &str,
folder_id: Option<String>, folder_id: Option<String>,
) -> Result<FileDto, DomainError> { ) -> Result<FileDto, DomainError> {
info!("Moving file with ID: {} to folder: {:?}", file_id, folder_id); info!(
"Moving file with ID: {} to folder: {:?}",
file_id, folder_id
);
let moved_file = self.file_repository.move_file(file_id, folder_id).await.map_err(|e| { let moved_file = self
error!("Error moving file (ID: {}): {}", file_id, e); .file_repository
e .move_file(file_id, folder_id)
})?; .await
.map_err(|e| {
error!("Error moving file (ID: {}): {}", file_id, e);
e
})?;
info!( info!(
"File moved successfully: {} (ID: {}) to folder: {:?}", "File moved successfully: {} (ID: {}) to folder: {:?}",
@@ -109,17 +121,17 @@ impl FileManagementUseCase for FileManagementService {
Ok(FileDto::from(moved_file)) Ok(FileDto::from(moved_file))
} }
async fn rename_file( async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<FileDto, DomainError> {
&self,
file_id: &str,
new_name: &str,
) -> Result<FileDto, DomainError> {
info!("Renaming file with ID: {} to \"{}\"", file_id, new_name); info!("Renaming file with ID: {} to \"{}\"", file_id, new_name);
let renamed_file = self.file_repository.rename_file(file_id, new_name).await.map_err(|e| { let renamed_file = self
error!("Error renaming file (ID: {}): {}", file_id, e); .file_repository
e .rename_file(file_id, new_name)
})?; .await
.map_err(|e| {
error!("Error renaming file (ID: {}): {}", file_id, e);
e
})?;
info!( info!(
"File renamed successfully: {} (ID: {})", "File renamed successfully: {} (ID: {})",
@@ -135,11 +147,7 @@ impl FileManagementUseCase for FileManagementService {
} }
/// Smart delete: trash-first with dedup reference cleanup. /// Smart delete: trash-first with dedup reference cleanup.
async fn delete_with_cleanup( async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result<bool, DomainError> {
&self,
id: &str,
user_id: &str,
) -> Result<bool, DomainError> {
// Step 1: Compute content hash for dedup tracking // Step 1: Compute content hash for dedup tracking
let content_hash = self.compute_content_hash(id).await; let content_hash = self.compute_content_hash(id).await;
@@ -175,4 +183,4 @@ impl FileManagementUseCase for FileManagementService {
Ok(false) // permanently deleted Ok(false) // permanently deleted
} }
} }
@@ -1,12 +1,12 @@
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use futures::Stream; use futures::Stream;
use std::sync::Arc;
use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::file_dto::FileDto;
use crate::application::ports::cache_ports::{ContentCachePort, WriteBehindCachePort};
use crate::application::ports::file_ports::{FileRetrievalUseCase, OptimizedFileContent}; use crate::application::ports::file_ports::{FileRetrievalUseCase, OptimizedFileContent};
use crate::application::ports::storage_ports::FileReadPort; use crate::application::ports::storage_ports::FileReadPort;
use crate::application::ports::cache_ports::{WriteBehindCachePort, ContentCachePort};
use crate::application::ports::transcode_ports::{ImageTranscodePort, OutputFormat}; use crate::application::ports::transcode_ports::{ImageTranscodePort, OutputFormat};
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
@@ -114,7 +114,10 @@ impl FileRetrievalUseCase for FileRetrievalService {
} }
} }
Err(DomainError::not_found("File", format!("not found at path: {}", path))) Err(DomainError::not_found(
"File",
format!("not found at path: {}", path),
))
} }
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError> { async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError> {
@@ -150,44 +153,69 @@ impl FileRetrievalUseCase for FileRetrievalService {
// ── Tier 0: Write-behind cache ─────────────────────── // ── Tier 0: Write-behind cache ───────────────────────
if let Some(wb) = &self.write_behind if let Some(wb) = &self.write_behind
&& let Some(pending) = wb.get_pending(id).await { && let Some(pending) = wb.get_pending(id).await
debug!("⚡ TIER 0 Write-Behind HIT: {} ({} bytes)", file_name, pending.len()); {
let (data, mime) = if do_transcode { debug!(
if let Some((t, m)) = self.try_transcode(id, &pending, &mime_type, file_size, true).await { "⚡ TIER 0 Write-Behind HIT: {} ({} bytes)",
(t, m) file_name,
} else { pending.len()
(pending, mime_type.clone()) );
} let (data, mime) = if do_transcode {
if let Some((t, m)) = self
.try_transcode(id, &pending, &mime_type, file_size, true)
.await
{
(t, m)
} else { } else {
(pending, mime_type.clone()) (pending, mime_type.clone())
}; }
return Ok((dto, OptimizedFileContent::Bytes { } else {
(pending, mime_type.clone())
};
return Ok((
dto,
OptimizedFileContent::Bytes {
data, data,
mime_type: mime, mime_type: mime,
was_transcoded: do_transcode, was_transcoded: do_transcode,
})); },
} ));
}
// ── Tier 1: Hot cache + transcode (<10 MB) ────────── // ── Tier 1: Hot cache + transcode (<10 MB) ──────────
if file_size < CACHE_THRESHOLD { if file_size < CACHE_THRESHOLD {
// Check content cache first // Check content cache first
if let Some(cache) = &self.content_cache if let Some(cache) = &self.content_cache
&& let Some((cached, _etag, _ct)) = cache.get(id).await { && let Some((cached, _etag, _ct)) = cache.get(id).await
debug!("🔥 TIER 1 Cache HIT: {} ({} bytes)", file_name, cached.len()); {
if do_transcode debug!(
&& let Some((t, m)) = self.try_transcode(id, &cached, &mime_type, file_size, true).await { "🔥 TIER 1 Cache HIT: {} ({} bytes)",
return Ok((dto, OptimizedFileContent::Bytes { file_name,
data: t, cached.len()
mime_type: m, );
was_transcoded: true, if do_transcode
})); && let Some((t, m)) = self
} .try_transcode(id, &cached, &mime_type, file_size, true)
return Ok((dto, OptimizedFileContent::Bytes { .await
{
return Ok((
dto,
OptimizedFileContent::Bytes {
data: t,
mime_type: m,
was_transcoded: true,
},
));
}
return Ok((
dto,
OptimizedFileContent::Bytes {
data: cached, data: cached,
mime_type: mime_type.clone(), mime_type: mime_type.clone(),
was_transcoded: false, was_transcoded: false,
})); },
} ));
}
// Cache miss – load from disk // Cache miss – load from disk
debug!("💾 TIER 1 Cache MISS: {} – loading from disk", file_name); debug!("💾 TIER 1 Cache MISS: {} – loading from disk", file_name);
@@ -197,27 +225,47 @@ impl FileRetrievalUseCase for FileRetrievalService {
// Store in cache // Store in cache
if let Some(cache) = &self.content_cache { if let Some(cache) = &self.content_cache {
let etag = format!("\"{}-{}\"", id, modified_at); let etag = format!("\"{}-{}\"", id, modified_at);
cache.put(id.to_string(), content_bytes.clone(), etag, mime_type.clone()).await; cache
.put(
id.to_string(),
content_bytes.clone(),
etag,
mime_type.clone(),
)
.await;
} }
if do_transcode if do_transcode
&& let Some((t, m)) = self.try_transcode(id, &content_bytes, &mime_type, file_size, true).await { && let Some((t, m)) = self
return Ok((dto, OptimizedFileContent::Bytes { .try_transcode(id, &content_bytes, &mime_type, file_size, true)
.await
{
return Ok((
dto,
OptimizedFileContent::Bytes {
data: t, data: t,
mime_type: m, mime_type: m,
was_transcoded: true, was_transcoded: true,
})); },
} ));
return Ok((dto, OptimizedFileContent::Bytes { }
data: content_bytes, return Ok((
mime_type: mime_type.clone(), dto,
was_transcoded: false, OptimizedFileContent::Bytes {
})); data: content_bytes,
mime_type: mime_type.clone(),
was_transcoded: false,
},
));
} }
// ── Tier 2: MMAP (10–100 MB) ──────────────────────── // ── Tier 2: MMAP (10–100 MB) ────────────────────────
if file_size < MMAP_THRESHOLD { if file_size < MMAP_THRESHOLD {
info!("🗺️ TIER 2 MMAP: {} ({} MB)", file_name, file_size / (1024 * 1024)); info!(
"🗺️ TIER 2 MMAP: {} ({} MB)",
file_name,
file_size / (1024 * 1024)
);
match self.file_read.get_file_mmap(id).await { match self.file_read.get_file_mmap(id).await {
Ok(mmap_content) => { Ok(mmap_content) => {
return Ok((dto, OptimizedFileContent::Mmap(mmap_content))); return Ok((dto, OptimizedFileContent::Mmap(mmap_content)));
@@ -230,17 +278,24 @@ impl FileRetrievalUseCase for FileRetrievalService {
} }
// ── Tier 3: Streaming (≥100 MB) ───────────────────── // ── Tier 3: Streaming (≥100 MB) ─────────────────────
info!("📡 TIER 3 STREAMING: {} ({} MB)", file_name, file_size / (1024 * 1024)); info!(
"📡 TIER 3 STREAMING: {} ({} MB)",
file_name,
file_size / (1024 * 1024)
);
match self.file_read.get_file_stream(id).await { match self.file_read.get_file_stream(id).await {
Ok(stream) => Ok((dto, OptimizedFileContent::Stream(Box::into_pin(stream)))), Ok(stream) => Ok((dto, OptimizedFileContent::Stream(Box::into_pin(stream)))),
Err(e) => { Err(e) => {
warn!("Streaming failed, last-resort content load: {}", e); warn!("Streaming failed, last-resort content load: {}", e);
let content = self.file_read.get_file_content(id).await?; let content = self.file_read.get_file_content(id).await?;
Ok((dto, OptimizedFileContent::Bytes { Ok((
data: Bytes::from(content), dto,
mime_type: mime_type.clone(), OptimizedFileContent::Bytes {
was_transcoded: false, data: Bytes::from(content),
})) mime_type: mime_type.clone(),
was_transcoded: false,
},
))
} }
} }
} }
@@ -254,4 +309,4 @@ impl FileRetrievalUseCase for FileRetrievalService {
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> { ) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
self.file_read.get_file_range_stream(id, start, end).await self.file_read.get_file_range_stream(id, start, end).await
} }
} }
+54 -37
View File
@@ -1,14 +1,14 @@
use std::sync::Arc;
use std::pin::Pin;
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use futures::Stream; use futures::Stream;
use std::pin::Pin;
use std::sync::Arc;
use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::file_dto::FileDto;
use crate::application::ports::file_ports::{FileUploadUseCase, UploadStrategy};
use crate::application::ports::storage_ports::{FileWritePort, FileReadPort};
use crate::application::ports::cache_ports::WriteBehindCachePort; use crate::application::ports::cache_ports::WriteBehindCachePort;
use crate::application::ports::dedup_ports::DedupPort; use crate::application::ports::dedup_ports::DedupPort;
use crate::application::ports::file_ports::{FileUploadUseCase, UploadStrategy};
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
@@ -47,7 +47,8 @@ pub struct FileUploadService {
/// Optional dedup service for content-addressable storage /// Optional dedup service for content-addressable storage
dedup: Option<Arc<dyn DedupPort>>, dedup: Option<Arc<dyn DedupPort>>,
/// Optional storage usage tracking /// Optional storage usage tracking
storage_usage_service: Option<Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>>, storage_usage_service:
Option<Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>>,
} }
impl FileUploadService { impl FileUploadService {
@@ -92,7 +93,10 @@ impl FileUploadService {
/// Run dedup tracking (non-fatal on failure). /// Run dedup tracking (non-fatal on failure).
async fn run_dedup(&self, data: &[u8], content_type: &str) { async fn run_dedup(&self, data: &[u8], content_type: &str) {
let Some(dedup) = &self.dedup else { return }; let Some(dedup) = &self.dedup else { return };
match dedup.store_bytes(data, Some(content_type.to_string())).await { match dedup
.store_bytes(data, Some(content_type.to_string()))
.await
{
Ok(result) => { Ok(result) => {
if result.was_deduplicated() { if result.was_deduplicated() {
info!( info!(
@@ -101,7 +105,10 @@ impl FileUploadService {
result.size() result.size()
); );
} else { } else {
info!("💾 DEDUP: new content stored (hash: {})", &result.hash()[..12]); info!(
"💾 DEDUP: new content stored (hash: {})",
&result.hash()[..12]
);
} }
} }
Err(e) => { Err(e) => {
@@ -119,7 +126,10 @@ impl FileUploadService {
let service_clone = Arc::clone(storage_service); let service_clone = Arc::clone(storage_service);
tokio::spawn(async move { tokio::spawn(async move {
match service_clone.update_user_storage_usage(&username).await { match service_clone.update_user_storage_usage(&username).await {
Ok(usage) => debug!("Updated storage usage for user {} to {} bytes", username, usage), Ok(usage) => debug!(
"Updated storage usage for user {} to {} bytes",
username, usage
),
Err(e) => warn!("Failed to update storage usage for {}: {}", username, e), Err(e) => warn!("Failed to update storage usage for {}: {}", username, e),
} }
}); });
@@ -138,7 +148,10 @@ impl FileUploadUseCase for FileUploadService {
content_type: String, content_type: String,
content: Vec<u8>, content: Vec<u8>,
) -> Result<FileDto, DomainError> { ) -> Result<FileDto, DomainError> {
let file = self.file_write.save_file(name, folder_id, content_type, content).await?; let file = self
.file_write
.save_file(name, folder_id, content_type, content)
.await?;
let dto = FileDto::from(file); let dto = FileDto::from(file);
self.maybe_update_storage_usage(&dto); self.maybe_update_storage_usage(&dto);
Ok(dto) Ok(dto)
@@ -170,34 +183,38 @@ impl FileUploadUseCase for FileUploadService {
// ─── TIER 1: Write-Behind (<256 KB) ────────────────── // ─── TIER 1: Write-Behind (<256 KB) ──────────────────
if total_size < WRITE_BEHIND_THRESHOLD if total_size < WRITE_BEHIND_THRESHOLD
&& let Some(wb) = &self.write_behind && let Some(wb) = &self.write_behind
&& wb.is_eligible_size(total_size) { && wb.is_eligible_size(total_size)
let data: Bytes = if chunks.len() == 1 { {
chunks.into_iter().next().unwrap() let data: Bytes = if chunks.len() == 1 {
} else { chunks.into_iter().next().unwrap()
let mut combined = Vec::with_capacity(total_size); } else {
for chunk in chunks { let mut combined = Vec::with_capacity(total_size);
combined.extend_from_slice(&chunk); for chunk in chunks {
} combined.extend_from_slice(&chunk);
combined.into()
};
let (file, target_path) = self
.file_write
.register_file_deferred(name.clone(), folder_id, content_type, total_size as u64)
.await?;
let dto = FileDto::from(file);
if let Err(e) = wb.put_pending(dto.id.clone(), data, target_path).await {
return Err(DomainError::internal_error("file", format!(
"Write-behind cache failed: {}",
e
)));
}
info!("⚡ WRITE-BEHIND UPLOAD: {} (ID: {}, ~0ms latency)", name, dto.id);
self.maybe_update_storage_usage(&dto);
return Ok((dto, UploadStrategy::WriteBehind));
} }
combined.into()
};
let (file, target_path) = self
.file_write
.register_file_deferred(name.clone(), folder_id, content_type, total_size as u64)
.await?;
let dto = FileDto::from(file);
if let Err(e) = wb.put_pending(dto.id.clone(), data, target_path).await {
return Err(DomainError::internal_error(
"file",
format!("Write-behind cache failed: {}", e),
));
}
info!(
"⚡ WRITE-BEHIND UPLOAD: {} (ID: {}, ~0ms latency)",
name, dto.id
);
self.maybe_update_storage_usage(&dto);
return Ok((dto, UploadStrategy::WriteBehind));
}
// ─── TIER 2: Streaming (≥1 MB) ────────────────────── // ─── TIER 2: Streaming (≥1 MB) ──────────────────────
if total_size >= STREAMING_UPLOAD_THRESHOLD { if total_size >= STREAMING_UPLOAD_THRESHOLD {
@@ -310,4 +327,4 @@ impl FileUploadUseCase for FileUploadService {
.await?; .await?;
Ok(()) Ok(())
} }
} }
@@ -1,10 +1,12 @@
use std::sync::Arc; use std::sync::Arc;
use crate::application::ports::file_ports::{FileUploadUseCase, FileRetrievalUseCase, FileManagementUseCase, FileUseCaseFactory}; use crate::application::ports::file_ports::{
use crate::application::services::file_upload_service::FileUploadService; FileManagementUseCase, FileRetrievalUseCase, FileUploadUseCase, FileUseCaseFactory,
use crate::application::services::file_retrieval_service::FileRetrievalService; };
use crate::application::services::file_management_service::FileManagementService;
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort}; use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::application::services::file_management_service::FileManagementService;
use crate::application::services::file_retrieval_service::FileRetrievalService;
use crate::application::services::file_upload_service::FileUploadService;
/// Factory for creating file use case implementations /// Factory for creating file use case implementations
pub struct AppFileUseCaseFactory { pub struct AppFileUseCaseFactory {
@@ -16,7 +18,7 @@ impl AppFileUseCaseFactory {
/// Creates a new factory for file use cases /// Creates a new factory for file use cases
pub fn new( pub fn new(
file_read_repository: Arc<dyn FileReadPort>, file_read_repository: Arc<dyn FileReadPort>,
file_write_repository: Arc<dyn FileWritePort> file_write_repository: Arc<dyn FileWritePort>,
) -> Self { ) -> Self {
Self { Self {
file_read_repository, file_read_repository,
@@ -29,12 +31,14 @@ impl FileUseCaseFactory for AppFileUseCaseFactory {
fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase> { fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase> {
Arc::new(FileUploadService::new(self.file_write_repository.clone())) Arc::new(FileUploadService::new(self.file_write_repository.clone()))
} }
fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase> { fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase> {
Arc::new(FileRetrievalService::new(self.file_read_repository.clone())) Arc::new(FileRetrievalService::new(self.file_read_repository.clone()))
} }
fn create_file_management_use_case(&self) -> Arc<dyn FileManagementUseCase> { fn create_file_management_use_case(&self) -> Arc<dyn FileManagementUseCase> {
Arc::new(FileManagementService::new(self.file_write_repository.clone())) Arc::new(FileManagementService::new(
self.file_write_repository.clone(),
))
} }
} }
+205 -122
View File
@@ -1,11 +1,13 @@
use std::sync::Arc; use crate::application::dtos::folder_dto::{
use async_trait::async_trait; CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
use crate::domain::services::path_service::StoragePath; };
use crate::application::dtos::folder_dto::{CreateFolderDto, RenameFolderDto, MoveFolderDto, FolderDto};
use crate::application::ports::inbound::FolderUseCase; use crate::application::ports::inbound::FolderUseCase;
use crate::application::ports::outbound::FolderStoragePort; use crate::application::ports::outbound::FolderStoragePort;
use crate::application::transactions::storage_transaction::StorageTransaction; use crate::application::transactions::storage_transaction::StorageTransaction;
use crate::common::errors::{DomainError, ErrorKind}; use crate::common::errors::{DomainError, ErrorKind};
use crate::domain::services::path_service::StoragePath;
use async_trait::async_trait;
use std::sync::Arc;
/// Implementation of the use case for folder operations /// Implementation of the use case for folder operations
pub struct FolderService { pub struct FolderService {
@@ -17,55 +19,71 @@ impl FolderService {
pub fn new(folder_storage: Arc<dyn FolderStoragePort>) -> Self { pub fn new(folder_storage: Arc<dyn FolderStoragePort>) -> Self {
Self { folder_storage } Self { folder_storage }
} }
/// Creates a stub implementation for testing and middleware /// Creates a stub implementation for testing and middleware
pub fn new_stub() -> impl FolderUseCase { pub fn new_stub() -> impl FolderUseCase {
struct FolderServiceStub; struct FolderServiceStub;
#[async_trait] #[async_trait]
impl FolderUseCase for FolderServiceStub { impl FolderUseCase for FolderServiceStub {
async fn create_folder(&self, _dto: CreateFolderDto) -> Result<FolderDto, DomainError> { async fn create_folder(&self, _dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty()) Ok(FolderDto::empty())
} }
async fn get_folder(&self, _id: &str) -> Result<FolderDto, DomainError> { async fn get_folder(&self, _id: &str) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty()) Ok(FolderDto::empty())
} }
async fn get_folder_by_path(&self, _path: &str) -> Result<FolderDto, DomainError> { async fn get_folder_by_path(&self, _path: &str) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty()) Ok(FolderDto::empty())
} }
async fn list_folders(&self, _parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> { async fn list_folders(
&self,
_parent_id: Option<&str>,
) -> Result<Vec<FolderDto>, DomainError> {
Ok(vec![]) Ok(vec![])
} }
async fn list_folders_paginated( async fn list_folders_paginated(
&self, &self,
_parent_id: Option<&str>, _parent_id: Option<&str>,
_pagination: &crate::application::dtos::pagination::PaginationRequestDto _pagination: &crate::application::dtos::pagination::PaginationRequestDto,
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError> { ) -> Result<
Ok(crate::application::dtos::pagination::PaginatedResponseDto::new( crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>,
vec![], DomainError,
0, > {
10, Ok(
0 crate::application::dtos::pagination::PaginatedResponseDto::new(
)) vec![],
0,
10,
0,
),
)
} }
async fn rename_folder(&self, _id: &str, _dto: RenameFolderDto) -> Result<FolderDto, DomainError> { async fn rename_folder(
&self,
_id: &str,
_dto: RenameFolderDto,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty()) Ok(FolderDto::empty())
} }
async fn move_folder(&self, _id: &str, _dto: MoveFolderDto) -> Result<FolderDto, DomainError> { async fn move_folder(
&self,
_id: &str,
_dto: MoveFolderDto,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty()) Ok(FolderDto::empty())
} }
async fn delete_folder(&self, _id: &str) -> Result<(), DomainError> { async fn delete_folder(&self, _id: &str) -> Result<(), DomainError> {
Ok(()) Ok(())
} }
} }
FolderServiceStub FolderServiceStub
} }
} }
@@ -79,10 +97,10 @@ impl FolderUseCase for FolderService {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"Folder", "Folder",
"Folder name cannot be empty" "Folder name cannot be empty",
)); ));
} }
// If a parent_id is provided, verify it exists // If a parent_id is provided, verify it exists
if let Some(parent_id) = &dto.parent_id { if let Some(parent_id) = &dto.parent_id {
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok(); let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
@@ -90,105 +108,147 @@ impl FolderUseCase for FolderService {
return Err(DomainError::not_found("Folder", parent_id)); return Err(DomainError::not_found("Folder", parent_id));
} }
} }
// Create the folder // Create the folder
let folder = self.folder_storage.create_folder(dto.name, dto.parent_id) let folder = self
.folder_storage
.create_folder(dto.name, dto.parent_id)
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to create folder: {}", e)))?; .map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to create folder: {}", e),
)
})?;
// Convert to DTO // Convert to DTO
Ok(FolderDto::from(folder)) Ok(FolderDto::from(folder))
} }
/// Gets a folder by its ID /// Gets a folder by its ID
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> { async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
let folder = self.folder_storage.get_folder(id) let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
.await DomainError::internal_error(
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {}: {}", id, e)))?; "FolderStorage",
format!("Failed to get folder with ID: {}: {}", id, e),
)
})?;
Ok(FolderDto::from(folder)) Ok(FolderDto::from(folder))
} }
/// Gets a folder by its path /// Gets a folder by its path
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError> { async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError> {
// Convert the string path to StoragePath // Convert the string path to StoragePath
let storage_path = StoragePath::from_string(path); let storage_path = StoragePath::from_string(path);
let folder = self.folder_storage.get_folder_by_path(&storage_path) let folder = self
.folder_storage
.get_folder_by_path(&storage_path)
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder at path: {}: {}", path, e)))?; .map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to get folder at path: {}: {}", path, e),
)
})?;
Ok(FolderDto::from(folder)) Ok(FolderDto::from(folder))
} }
/// Lists folders within a parent folder /// Lists folders within a parent folder
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> { async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> {
let folders = self.folder_storage.list_folders(parent_id) let folders = self
.folder_storage
.list_folders(parent_id)
.await .await
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders in parent: {:?}: {}", parent_id, e)))?; .map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!("Failed to list folders in parent: {:?}: {}", parent_id, e),
)
})?;
// Convert to DTOs // Convert to DTOs
Ok(folders.into_iter().map(FolderDto::from).collect()) Ok(folders.into_iter().map(FolderDto::from).collect())
} }
/// Lists folders with pagination /// Lists folders with pagination
async fn list_folders_paginated( async fn list_folders_paginated(
&self, &self,
parent_id: Option<&str>, parent_id: Option<&str>,
pagination: &crate::application::dtos::pagination::PaginationRequestDto pagination: &crate::application::dtos::pagination::PaginationRequestDto,
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError> { ) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>
{
// Validate and adjust pagination // Validate and adjust pagination
let pagination = pagination.validate_and_adjust(); let pagination = pagination.validate_and_adjust();
// Get paginated folders and total count // Get paginated folders and total count
let (folders, total_items) = self.folder_storage.list_folders_paginated( let (folders, total_items) = self
parent_id, .folder_storage
pagination.offset(), .list_folders_paginated(
pagination.limit(), parent_id,
true // Always include total for better UX pagination.offset(),
) pagination.limit(),
.await true, // Always include total for better UX
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders with pagination in parent: {:?}: {}", parent_id, e)))?; )
.await
.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!(
"Failed to list folders with pagination in parent: {:?}: {}",
parent_id, e
),
)
})?;
// The total is needed to calculate pagination // The total is needed to calculate pagination
let total = total_items.unwrap_or(folders.len()); let total = total_items.unwrap_or(folders.len());
// Convert to PaginatedResponseDto // Convert to PaginatedResponseDto
let response = crate::application::dtos::pagination::PaginatedResponseDto::new( let response = crate::application::dtos::pagination::PaginatedResponseDto::new(
folders.into_iter().map(FolderDto::from).collect(), folders.into_iter().map(FolderDto::from).collect(),
pagination.page, pagination.page,
pagination.page_size, pagination.page_size,
total total,
); );
Ok(response) Ok(response)
} }
/// Renames a folder /// Renames a folder
async fn rename_folder(&self, id: &str, dto: RenameFolderDto) -> Result<FolderDto, DomainError> { async fn rename_folder(
&self,
id: &str,
dto: RenameFolderDto,
) -> Result<FolderDto, DomainError> {
// Input validation // Input validation
if dto.name.is_empty() { if dto.name.is_empty() {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"Folder", "Folder",
"New folder name cannot be empty" "New folder name cannot be empty",
)); ));
} }
// Verify the folder exists // Verify the folder exists
let existing_folder = self.folder_storage.get_folder(id) let existing_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
.await DomainError::internal_error(
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for renaming: {}", id, e)))?; "FolderStorage",
format!("Failed to get folder with ID: {} for renaming: {}", id, e),
)
})?;
// Create transaction for renaming // Create transaction for renaming
let mut transaction = StorageTransaction::new("rename_folder"); let mut transaction = StorageTransaction::new("rename_folder");
// Main operation: rename folder // Main operation: rename folder
// Clone all values to avoid lifetime issues // Clone all values to avoid lifetime issues
let folder_storage = self.folder_storage.clone(); let folder_storage = self.folder_storage.clone();
let id_owned = id.to_string(); let id_owned = id.to_string();
let name_owned = dto.name.clone(); let name_owned = dto.name.clone();
// Create future with owned values // Create future with owned values
let rename_op = async move { let rename_op = async move {
folder_storage.rename_folder(&id_owned, name_owned).await?; folder_storage.rename_folder(&id_owned, name_owned).await?;
@@ -198,40 +258,50 @@ impl FolderUseCase for FolderService {
let original_name = existing_folder.name().to_string(); let original_name = existing_folder.name().to_string();
let storage = self.folder_storage.clone(); let storage = self.folder_storage.clone();
let id_clone = id.to_string(); let id_clone = id.to_string();
async move { async move {
// In case of failure, restore the original name // In case of failure, restore the original name
storage.rename_folder(&id_clone, original_name).await storage
.rename_folder(&id_clone, original_name)
.await
.map(|_| ()) .map(|_| ())
.map_err(|e| DomainError::new( .map_err(|e| {
ErrorKind::InternalError, DomainError::new(
"Folder", ErrorKind::InternalError,
format!("Failed to rollback folder rename: {}", e) "Folder",
)) format!("Failed to rollback folder rename: {}", e),
)
})
} }
}; };
// Add to the transaction // Add to the transaction
transaction.add_operation(rename_op, rollback_op); transaction.add_operation(rename_op, rollback_op);
// Execute transaction // Execute transaction
transaction.commit().await?; transaction.commit().await?;
// Get the renamed folder // Get the renamed folder
let folder = self.folder_storage.get_folder(id) let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
.await DomainError::internal_error(
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get renamed folder with ID: {}: {}", id, e)))?; "FolderStorage",
format!("Failed to get renamed folder with ID: {}: {}", id, e),
)
})?;
Ok(FolderDto::from(folder)) Ok(FolderDto::from(folder))
} }
/// Moves a folder to a new parent /// Moves a folder to a new parent
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError> { async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError> {
// Verify the source folder exists // Verify the source folder exists
let source_folder = self.folder_storage.get_folder(id) let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
.await DomainError::internal_error(
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for moving: {}", id, e)))?; "FolderStorage",
format!("Failed to get folder with ID: {} for moving: {}", id, e),
)
})?;
// If a parent_id is specified, verify it exists // If a parent_id is specified, verify it exists
if let Some(parent_id) = &dto.parent_id { if let Some(parent_id) = &dto.parent_id {
// Verify we are not trying to move the folder into itself or one of its descendants // Verify we are not trying to move the folder into itself or one of its descendants
@@ -239,29 +309,29 @@ impl FolderUseCase for FolderService {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"Folder", "Folder",
"Cannot move a folder into itself" "Cannot move a folder into itself",
)); ));
} }
// Verify the destination exists // Verify the destination exists
let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok(); let parent_exists = self.folder_storage.get_folder(parent_id).await.is_ok();
if !parent_exists { if !parent_exists {
return Err(DomainError::not_found("Folder", parent_id)); return Err(DomainError::not_found("Folder", parent_id));
} }
// TODO: Ideally we should verify the entire hierarchy to prevent cycles // TODO: Ideally we should verify the entire hierarchy to prevent cycles
} }
// Create transaction for moving // Create transaction for moving
let mut transaction = StorageTransaction::new("move_folder"); let mut transaction = StorageTransaction::new("move_folder");
// Main operation: move folder // Main operation: move folder
// Clone all values to avoid lifetime issues // Clone all values to avoid lifetime issues
let folder_storage = self.folder_storage.clone(); let folder_storage = self.folder_storage.clone();
let id_owned = id.to_string(); let id_owned = id.to_string();
// Get parent ID as owned string or None // Get parent ID as owned string or None
let parent_id_owned = dto.parent_id.as_ref().map(|p| p.to_string()); let parent_id_owned = dto.parent_id.as_ref().map(|p| p.to_string());
// Create future with owned values // Create future with owned values
let move_op = async move { let move_op = async move {
// Convert Option<String> to Option<&str> // Convert Option<String> to Option<&str>
@@ -273,45 +343,58 @@ impl FolderUseCase for FolderService {
let original_parent_id = source_folder.parent_id().map(String::from); let original_parent_id = source_folder.parent_id().map(String::from);
let storage = self.folder_storage.clone(); let storage = self.folder_storage.clone();
let id_clone = id.to_string(); let id_clone = id.to_string();
async move { async move {
// In case of failure, restore the original location // In case of failure, restore the original location
storage.move_folder(&id_clone, original_parent_id.as_deref()).await storage
.move_folder(&id_clone, original_parent_id.as_deref())
.await
.map(|_| ()) .map(|_| ())
.map_err(|e| DomainError::new( .map_err(|e| {
ErrorKind::InternalError, DomainError::new(
"Folder", ErrorKind::InternalError,
format!("Failed to rollback folder move: {}", e) "Folder",
)) format!("Failed to rollback folder move: {}", e),
)
})
} }
}; };
// Add to the transaction // Add to the transaction
transaction.add_operation(move_op, rollback_op); transaction.add_operation(move_op, rollback_op);
// Execute transaction // Execute transaction
transaction.commit().await?; transaction.commit().await?;
// Get the moved folder // Get the moved folder
let folder = self.folder_storage.get_folder(id) let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
.await DomainError::internal_error(
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get moved folder with ID: {}: {}", id, e)))?; "FolderStorage",
format!("Failed to get moved folder with ID: {}: {}", id, e),
)
})?;
Ok(FolderDto::from(folder)) Ok(FolderDto::from(folder))
} }
/// Deletes a folder /// Deletes a folder
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> { async fn delete_folder(&self, id: &str) -> Result<(), DomainError> {
// Verify the folder exists // Verify the folder exists
let _folder = self.folder_storage.get_folder(id) let _folder = self.folder_storage.get_folder(id).await.map_err(|e| {
.await DomainError::internal_error(
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for deletion: {}", id, e)))?; "FolderStorage",
format!("Failed to get folder with ID: {} for deletion: {}", id, e),
)
})?;
// In a real implementation, we could verify permissions, dependencies, etc. // In a real implementation, we could verify permissions, dependencies, etc.
// Delete the folder // Delete the folder
self.folder_storage.delete_folder(id) self.folder_storage.delete_folder(id).await.map_err(|e| {
.await DomainError::internal_error(
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to delete folder with ID: {}: {}", id, e))) "FolderStorage",
format!("Failed to delete folder with ID: {}: {}", id, e),
)
})
} }
} }
@@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use crate::domain::services::i18n_service::{I18nService, I18nResult, Locale}; use crate::domain::services::i18n_service::{I18nResult, I18nService, Locale};
/// Service for i18n operations /// Service for i18n operations
pub struct I18nApplicationService { pub struct I18nApplicationService {
@@ -11,65 +11,67 @@ impl I18nApplicationService {
/// Creates a dummy service for testing /// Creates a dummy service for testing
pub fn dummy() -> Self { pub fn dummy() -> Self {
struct DummyI18nService; struct DummyI18nService;
#[async_trait::async_trait] #[async_trait::async_trait]
impl I18nService for DummyI18nService { impl I18nService for DummyI18nService {
async fn translate(&self, _key: &str, _locale: Locale) -> I18nResult<String> { async fn translate(&self, _key: &str, _locale: Locale) -> I18nResult<String> {
Ok("DUMMY_TRANSLATION".to_string()) Ok("DUMMY_TRANSLATION".to_string())
} }
async fn load_translations(&self, _locale: Locale) -> I18nResult<()> { async fn load_translations(&self, _locale: Locale) -> I18nResult<()> {
Ok(()) Ok(())
} }
async fn available_locales(&self) -> Vec<Locale> { async fn available_locales(&self) -> Vec<Locale> {
vec![Locale::English, Locale::Spanish] vec![Locale::English, Locale::Spanish]
} }
async fn is_supported(&self, _locale: Locale) -> bool { async fn is_supported(&self, _locale: Locale) -> bool {
true true
} }
} }
Self { i18n_service: Arc::new(DummyI18nService) } Self {
i18n_service: Arc::new(DummyI18nService),
}
} }
/// Creates a new i18n application service /// Creates a new i18n application service
pub fn new(i18n_service: Arc<dyn I18nService>) -> Self { pub fn new(i18n_service: Arc<dyn I18nService>) -> Self {
Self { i18n_service } Self { i18n_service }
} }
/// Get a translation for a key and locale /// Get a translation for a key and locale
pub async fn translate(&self, key: &str, locale: Option<Locale>) -> I18nResult<String> { pub async fn translate(&self, key: &str, locale: Option<Locale>) -> I18nResult<String> {
let locale = locale.unwrap_or(Locale::default()); let locale = locale.unwrap_or(Locale::default());
self.i18n_service.translate(key, locale).await self.i18n_service.translate(key, locale).await
} }
/// Load translations for a locale /// Load translations for a locale
pub async fn load_translations(&self, locale: Locale) -> I18nResult<()> { pub async fn load_translations(&self, locale: Locale) -> I18nResult<()> {
self.i18n_service.load_translations(locale).await self.i18n_service.load_translations(locale).await
} }
/// Load translations for all available locales /// Load translations for all available locales
pub async fn load_all_translations(&self) -> Vec<(Locale, I18nResult<()>)> { pub async fn load_all_translations(&self) -> Vec<(Locale, I18nResult<()>)> {
let locales = self.i18n_service.available_locales().await; let locales = self.i18n_service.available_locales().await;
let mut results = Vec::new(); let mut results = Vec::new();
for locale in locales { for locale in locales {
let result = self.i18n_service.load_translations(locale).await; let result = self.i18n_service.load_translations(locale).await;
results.push((locale, result)); results.push((locale, result));
} }
results results
} }
/// Get available locales /// Get available locales
pub async fn available_locales(&self) -> Vec<Locale> { pub async fn available_locales(&self) -> Vec<Locale> {
self.i18n_service.available_locales().await self.i18n_service.available_locales().await
} }
/// Check if a locale is supported /// Check if a locale is supported
pub async fn is_supported(&self, locale: Locale) -> bool { pub async fn is_supported(&self, locale: Locale) -> bool {
self.i18n_service.is_supported(locale).await self.i18n_service.is_supported(locale).await
} }
} }
+2 -2
View File
@@ -21,7 +21,7 @@ pub mod trash_service;
mod trash_service_test; mod trash_service_test;
// Re-exportar para facilitar acceso // Re-exportar para facilitar acceso
pub use file_upload_service::FileUploadService;
pub use file_retrieval_service::FileRetrievalService;
pub use file_management_service::FileManagementService; pub use file_management_service::FileManagementService;
pub use file_retrieval_service::FileRetrievalService;
pub use file_upload_service::FileUploadService;
pub use file_use_case_factory::AppFileUseCaseFactory; pub use file_use_case_factory::AppFileUseCaseFactory;
+51 -16
View File
@@ -1,9 +1,9 @@
use std::sync::Arc;
use async_trait::async_trait;
use tracing::info;
use crate::common::errors::{Result, DomainError, ErrorKind};
use crate::application::ports::recent_ports::{RecentItemsUseCase, RecentItemsRepositoryPort};
use crate::application::dtos::recent_dto::RecentItemDto; use crate::application::dtos::recent_dto::RecentItemDto;
use crate::application::ports::recent_ports::{RecentItemsRepositoryPort, RecentItemsUseCase};
use crate::common::errors::{DomainError, ErrorKind, Result};
use async_trait::async_trait;
use std::sync::Arc;
use tracing::info;
/// Implementation of the use case for managing recent items. /// Implementation of the use case for managing recent items.
/// ///
@@ -27,17 +27,35 @@ impl RecentService {
#[async_trait] #[async_trait]
impl RecentItemsUseCase for RecentService { impl RecentItemsUseCase for RecentService {
/// Get recent items for a user /// Get recent items for a user
async fn get_recent_items(&self, user_id: &str, limit: Option<i32>) -> Result<Vec<RecentItemDto>> { async fn get_recent_items(
&self,
user_id: &str,
limit: Option<i32>,
) -> Result<Vec<RecentItemDto>> {
info!("Getting recent items for user: {}", user_id); info!("Getting recent items for user: {}", user_id);
let limit_value = limit.unwrap_or(self.max_recent_items).min(self.max_recent_items); let limit_value = limit
.unwrap_or(self.max_recent_items)
.min(self.max_recent_items);
let items = self.repo.get_recent_items(user_id, limit_value).await?; let items = self.repo.get_recent_items(user_id, limit_value).await?;
info!("Retrieved {} recent items for user {}", items.len(), user_id); info!(
"Retrieved {} recent items for user {}",
items.len(),
user_id
);
Ok(items) Ok(items)
} }
/// Record access to an item /// Record access to an item
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> { async fn record_item_access(
info!("Recording access to {} '{}' for user {}", item_type, item_id, user_id); &self,
user_id: &str,
item_id: &str,
item_type: &str,
) -> Result<()> {
info!(
"Recording access to {} '{}' for user {}",
item_type, item_id, user_id
);
if item_type != "file" && item_type != "folder" { if item_type != "file" && item_type != "folder" {
return Err(DomainError::new( return Err(DomainError::new(
@@ -50,18 +68,35 @@ impl RecentItemsUseCase for RecentService {
self.repo.upsert_access(user_id, item_id, item_type).await?; self.repo.upsert_access(user_id, item_id, item_type).await?;
self.repo.prune(user_id, self.max_recent_items).await?; self.repo.prune(user_id, self.max_recent_items).await?;
info!("Successfully recorded access to {} '{}' for user {}", item_type, item_id, user_id); info!(
"Successfully recorded access to {} '{}' for user {}",
item_type, item_id, user_id
);
Ok(()) Ok(())
} }
/// Remove an item from recent /// Remove an item from recent
async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> { async fn remove_from_recent(
info!("Removing {} '{}' from recent for user {}", item_type, item_id, user_id); &self,
user_id: &str,
item_id: &str,
item_type: &str,
) -> Result<bool> {
info!(
"Removing {} '{}' from recent for user {}",
item_type, item_id, user_id
);
let removed = self.repo.remove_item(user_id, item_id, item_type).await?; let removed = self.repo.remove_item(user_id, item_id, item_type).await?;
info!( info!(
"{} {} '{}' from recent items for user {}", "{} {} '{}' from recent items for user {}",
if removed { "Successfully removed" } else { "Not found" }, if removed {
item_type, item_id, user_id "Successfully removed"
} else {
"Not found"
},
item_type,
item_id,
user_id
); );
Ok(removed) Ok(removed)
} }
@@ -73,4 +108,4 @@ impl RecentItemsUseCase for RecentService {
info!("Cleared all recent items for user {}", user_id); info!("Cleared all recent items for user {}", user_id);
Ok(()) Ok(())
} }
} }
+194 -166
View File
@@ -1,21 +1,21 @@
use std::sync::Arc;
use std::collections::HashMap;
use std::time::{Duration, Instant};
use std::sync::Mutex;
use async_trait::async_trait; use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tokio::time; use tokio::time;
use crate::common::errors::Result;
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto; use crate::application::dtos::folder_dto::FolderDto;
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
use crate::application::ports::inbound::SearchUseCase; use crate::application::ports::inbound::SearchUseCase;
use crate::application::ports::outbound::FolderStoragePort; use crate::application::ports::outbound::FolderStoragePort;
use crate::application::ports::storage_ports::FileReadPort; use crate::application::ports::storage_ports::FileReadPort;
use crate::common::errors::Result;
/** /**
* Search service implementation for files and folders. * Search service implementation for files and folders.
* *
* This service implements the advanced search functionality that allows * This service implements the advanced search functionality that allows
* users to find files and folders based on various criteria * users to find files and folders based on various criteria
* such as name, type, date and size. It also includes a cache to improve * such as name, type, date and size. It also includes a cache to improve
@@ -24,16 +24,16 @@ use crate::application::ports::storage_ports::FileReadPort;
pub struct SearchService { pub struct SearchService {
/// Repository for file operations /// Repository for file operations
file_repository: Arc<dyn FileReadPort>, file_repository: Arc<dyn FileReadPort>,
/// Repository for folder operations /// Repository for folder operations
folder_repository: Arc<dyn FolderStoragePort>, folder_repository: Arc<dyn FolderStoragePort>,
/// Search results cache with expiration time /// Search results cache with expiration time
search_cache: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>, search_cache: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
/// Cache validity duration in seconds /// Cache validity duration in seconds
cache_ttl: u64, cache_ttl: u64,
/// Maximum cache size (number of stored results) /// Maximum cache size (number of stored results)
max_cache_size: usize, max_cache_size: usize,
} }
@@ -43,7 +43,7 @@ pub struct SearchService {
struct SearchCacheKey { struct SearchCacheKey {
/// Serialized representation of the search criteria /// Serialized representation of the search criteria
criteria_hash: String, criteria_hash: String,
/// User ID (to isolate searches between users) /// User ID (to isolate searches between users)
user_id: String, user_id: String,
} }
@@ -52,7 +52,7 @@ struct SearchCacheKey {
struct CachedSearchResult { struct CachedSearchResult {
/// Search results /// Search results
results: SearchResultsDto, results: SearchResultsDto,
/// Time when the cache entry was created /// Time when the cache entry was created
timestamp: Instant, timestamp: Instant,
} }
@@ -60,7 +60,7 @@ struct CachedSearchResult {
impl SearchService { impl SearchService {
/** /**
* Creates a new instance of the search service. * Creates a new instance of the search service.
* *
* @param file_repository Repository for file operations * @param file_repository Repository for file operations
* @param folder_repository Repository for folder operations * @param folder_repository Repository for folder operations
* @param cache_ttl Cache time-to-live in seconds (0 to disable) * @param cache_ttl Cache time-to-live in seconds (0 to disable)
@@ -79,18 +79,18 @@ impl SearchService {
cache_ttl, cache_ttl,
max_cache_size, max_cache_size,
}; };
// Start cache cleanup task if TTL > 0 // Start cache cleanup task if TTL > 0
if cache_ttl > 0 { if cache_ttl > 0 {
Self::start_cache_cleanup_task(search_service.search_cache.clone(), cache_ttl); Self::start_cache_cleanup_task(search_service.search_cache.clone(), cache_ttl);
} }
search_service search_service
} }
/** /**
* Starts an asynchronous task to clean up expired cache entries. * Starts an asynchronous task to clean up expired cache entries.
* *
* @param cache_ref Reference to the shared cache * @param cache_ref Reference to the shared cache
* @param ttl_seconds TTL in seconds * @param ttl_seconds TTL in seconds
*/ */
@@ -101,21 +101,21 @@ impl SearchService {
tokio::spawn(async move { tokio::spawn(async move {
let cleanup_interval = Duration::from_secs(ttl_seconds / 2); let cleanup_interval = Duration::from_secs(ttl_seconds / 2);
let ttl = Duration::from_secs(ttl_seconds); let ttl = Duration::from_secs(ttl_seconds);
loop { loop {
time::sleep(cleanup_interval).await; time::sleep(cleanup_interval).await;
// Acquire lock and clean up expired entries // Acquire lock and clean up expired entries
if let Ok(mut cache) = cache_ref.lock() { if let Ok(mut cache) = cache_ref.lock() {
let now = Instant::now(); let now = Instant::now();
// Identify expired entries // Identify expired entries
let expired_keys: Vec<SearchCacheKey> = cache let expired_keys: Vec<SearchCacheKey> = cache
.iter() .iter()
.filter(|(_, result)| now.duration_since(result.timestamp) > ttl) .filter(|(_, result)| now.duration_since(result.timestamp) > ttl)
.map(|(key, _)| key.clone()) .map(|(key, _)| key.clone())
.collect(); .collect();
// Remove expired entries // Remove expired entries
for key in expired_keys { for key in expired_keys {
cache.remove(&key); cache.remove(&key);
@@ -124,10 +124,10 @@ impl SearchService {
} }
}); });
} }
/** /**
* Creates a cache key from the search criteria. * Creates a cache key from the search criteria.
* *
* @param criteria Search criteria * @param criteria Search criteria
* @param user_id User ID (to isolate cache between users) * @param user_id User ID (to isolate cache between users)
* @return Cache key * @return Cache key
@@ -135,16 +135,16 @@ impl SearchService {
fn create_cache_key(&self, criteria: &SearchCriteriaDto, user_id: &str) -> SearchCacheKey { fn create_cache_key(&self, criteria: &SearchCriteriaDto, user_id: &str) -> SearchCacheKey {
// Serialize criteria to generate a hash // Serialize criteria to generate a hash
let criteria_str = serde_json::to_string(criteria).unwrap_or_default(); let criteria_str = serde_json::to_string(criteria).unwrap_or_default();
SearchCacheKey { SearchCacheKey {
criteria_hash: criteria_str, criteria_hash: criteria_str,
user_id: user_id.to_string(), user_id: user_id.to_string(),
} }
} }
/** /**
* Attempts to retrieve results from the cache. * Attempts to retrieve results from the cache.
* *
* @param key Cache key * @param key Cache key
* @return Optionally, the results if they exist and have not expired * @return Optionally, the results if they exist and have not expired
*/ */
@@ -153,24 +153,25 @@ impl SearchService {
if self.cache_ttl == 0 { if self.cache_ttl == 0 {
return None; return None;
} }
if let Ok(cache) = self.search_cache.lock() if let Ok(cache) = self.search_cache.lock()
&& let Some(cached_result) = cache.get(key) { && let Some(cached_result) = cache.get(key)
let now = Instant::now(); {
let ttl = Duration::from_secs(self.cache_ttl); let now = Instant::now();
let ttl = Duration::from_secs(self.cache_ttl);
// Check if the entry has expired
if now.duration_since(cached_result.timestamp) < ttl { // Check if the entry has expired
return Some(cached_result.results.clone()); if now.duration_since(cached_result.timestamp) < ttl {
} return Some(cached_result.results.clone());
} }
}
None None
} }
/** /**
* Stores results in the cache. * Stores results in the cache.
* *
* @param key Cache key * @param key Cache key
* @param results Results to store * @param results Results to store
*/ */
@@ -179,45 +180,56 @@ impl SearchService {
if self.cache_ttl == 0 { if self.cache_ttl == 0 {
return; return;
} }
if let Ok(mut cache) = self.search_cache.lock() { if let Ok(mut cache) = self.search_cache.lock() {
// If the cache is full, remove the oldest entry // If the cache is full, remove the oldest entry
if cache.len() >= self.max_cache_size if cache.len() >= self.max_cache_size
&& let Some((oldest_key, _)) = cache && let Some((oldest_key, _)) =
.iter() cache.iter().min_by_key(|(_, result)| result.timestamp)
.min_by_key(|(_, result)| result.timestamp) { {
let key_to_remove = oldest_key.clone(); let key_to_remove = oldest_key.clone();
cache.remove(&key_to_remove); cache.remove(&key_to_remove);
} }
// Store the new result // Store the new result
cache.insert(key, CachedSearchResult { cache.insert(
results, key,
timestamp: Instant::now(), CachedSearchResult {
}); results,
timestamp: Instant::now(),
},
);
} }
} }
/** /**
* Filters files according to the search criteria. * Filters files according to the search criteria.
* *
* @param files List of files to filter * @param files List of files to filter
* @param criteria Search criteria * @param criteria Search criteria
* @return Files that match the criteria * @return Files that match the criteria
*/ */
fn filter_files(&self, files: Vec<FileDto>, criteria: &SearchCriteriaDto) -> Vec<FileDto> { fn filter_files(&self, files: Vec<FileDto>, criteria: &SearchCriteriaDto) -> Vec<FileDto> {
files.into_iter() files
.into_iter()
.filter(|file| { .filter(|file| {
// Filter by name // Filter by name
if let Some(name_query) = &criteria.name_contains if let Some(name_query) = &criteria.name_contains
&& !file.name.to_lowercase().contains(&name_query.to_lowercase()) { && !file
return false; .name
} .to_lowercase()
.contains(&name_query.to_lowercase())
{
return false;
}
// Filter by file type (extension) // Filter by file type (extension)
if let Some(file_types) = &criteria.file_types { if let Some(file_types) = &criteria.file_types {
if let Some(extension) = file.name.split('.').next_back() { if let Some(extension) = file.name.split('.').next_back() {
if !file_types.iter().any(|ext| ext.eq_ignore_ascii_case(extension)) { if !file_types
.iter()
.any(|ext| ext.eq_ignore_ascii_case(extension))
{
return false; return false;
} }
} else { } else {
@@ -225,91 +237,110 @@ impl SearchService {
return false; return false;
} }
} }
// Filter by creation date // Filter by creation date
if let Some(created_after) = criteria.created_after if let Some(created_after) = criteria.created_after
&& file.created_at < created_after { && file.created_at < created_after
return false; {
} return false;
}
if let Some(created_before) = criteria.created_before if let Some(created_before) = criteria.created_before
&& file.created_at > created_before { && file.created_at > created_before
return false; {
} return false;
}
// Filter by modification date // Filter by modification date
if let Some(modified_after) = criteria.modified_after if let Some(modified_after) = criteria.modified_after
&& file.modified_at < modified_after { && file.modified_at < modified_after
return false; {
} return false;
}
if let Some(modified_before) = criteria.modified_before if let Some(modified_before) = criteria.modified_before
&& file.modified_at > modified_before { && file.modified_at > modified_before
return false; {
} return false;
}
// Filter by size // Filter by size
if let Some(min_size) = criteria.min_size if let Some(min_size) = criteria.min_size
&& file.size < min_size { && file.size < min_size
return false; {
} return false;
}
if let Some(max_size) = criteria.max_size if let Some(max_size) = criteria.max_size
&& file.size > max_size { && file.size > max_size
return false; {
} return false;
}
true true
}) })
.collect() .collect()
} }
/** /**
* Filters folders according to the search criteria. * Filters folders according to the search criteria.
* *
* @param folders List of folders to filter * @param folders List of folders to filter
* @param criteria Search criteria * @param criteria Search criteria
* @return Folders that match the criteria * @return Folders that match the criteria
*/ */
fn filter_folders(&self, folders: Vec<FolderDto>, criteria: &SearchCriteriaDto) -> Vec<FolderDto> { fn filter_folders(
folders.into_iter() &self,
folders: Vec<FolderDto>,
criteria: &SearchCriteriaDto,
) -> Vec<FolderDto> {
folders
.into_iter()
.filter(|folder| { .filter(|folder| {
// Filter by name // Filter by name
if let Some(name_query) = &criteria.name_contains if let Some(name_query) = &criteria.name_contains
&& !folder.name.to_lowercase().contains(&name_query.to_lowercase()) { && !folder
return false; .name
} .to_lowercase()
.contains(&name_query.to_lowercase())
{
return false;
}
// Filter by creation date // Filter by creation date
if let Some(created_after) = criteria.created_after if let Some(created_after) = criteria.created_after
&& folder.created_at < created_after { && folder.created_at < created_after
return false; {
} return false;
}
if let Some(created_before) = criteria.created_before if let Some(created_before) = criteria.created_before
&& folder.created_at > created_before { && folder.created_at > created_before
return false; {
} return false;
}
// Filter by modification date // Filter by modification date
if let Some(modified_after) = criteria.modified_after if let Some(modified_after) = criteria.modified_after
&& folder.modified_at < modified_after { && folder.modified_at < modified_after
return false; {
} return false;
}
if let Some(modified_before) = criteria.modified_before if let Some(modified_before) = criteria.modified_before
&& folder.modified_at > modified_before { && folder.modified_at > modified_before
return false; {
} return false;
}
true true
}) })
.collect() .collect()
} }
/** /**
* Implementation of recursive search through folders. * Implementation of recursive search through folders.
* *
* @param current_folder_id ID of the current folder * @param current_folder_id ID of the current folder
* @param criteria Search criteria * @param criteria Search criteria
* @param found_files Files found so far * @param found_files Files found so far
@@ -323,43 +354,39 @@ impl SearchService {
found_folders: &mut Vec<FolderDto>, found_folders: &mut Vec<FolderDto>,
) -> Result<()> { ) -> Result<()> {
Box::pin(async move { Box::pin(async move {
// List files in the current folder // List files in the current folder
let files = self.file_repository.list_files(current_folder_id).await?; let files = self.file_repository.list_files(current_folder_id).await?;
// Filter files according to criteria and add them to the results // Filter files according to criteria and add them to the results
let filtered_files = self.filter_files( let filtered_files =
files.into_iter().map(FileDto::from).collect(), self.filter_files(files.into_iter().map(FileDto::from).collect(), criteria);
criteria found_files.extend(filtered_files);
);
found_files.extend(filtered_files); // If the search is recursive, process subfolders
if criteria.recursive {
// If the search is recursive, process subfolders // List subfolders
if criteria.recursive { let folders = self
// List subfolders .folder_repository
let folders = self.folder_repository.list_folders(current_folder_id).await?; .list_folders(current_folder_id)
.await?;
// Filter folders according to criteria and add them to the results
let filtered_folders: Vec<FolderDto> = self.filter_folders( // Filter folders according to criteria and add them to the results
folders.into_iter().map(FolderDto::from).collect(), let filtered_folders: Vec<FolderDto> = self
criteria .filter_folders(folders.into_iter().map(FolderDto::from).collect(), criteria);
);
// Add filtered folders to the results
// Add filtered folders to the results found_folders.extend(filtered_folders.iter().cloned());
found_folders.extend(filtered_folders.iter().cloned());
// Search recursively in each subfolder
// Search recursively in each subfolder for folder in filtered_folders {
for folder in filtered_folders { self.search_recursive(Some(&folder.id), criteria, found_files, found_folders)
self.search_recursive( .await?;
Some(&folder.id), }
criteria,
found_files,
found_folders,
).await?;
} }
}
Ok(())
Ok(()) })
}).await .await
} }
} }
@@ -367,7 +394,7 @@ impl SearchService {
impl SearchUseCase for SearchService { impl SearchUseCase for SearchService {
/** /**
* Performs a search based on the specified criteria. * Performs a search based on the specified criteria.
* *
* @param criteria Search criteria * @param criteria Search criteria
* @return Search results * @return Search results
*/ */
@@ -375,36 +402,37 @@ impl SearchUseCase for SearchService {
// TODO: Get user ID from the authentication context // TODO: Get user ID from the authentication context
let user_id = "default-user"; let user_id = "default-user";
let cache_key = self.create_cache_key(&criteria, user_id); let cache_key = self.create_cache_key(&criteria, user_id);
// Try to get results from the cache // Try to get results from the cache
if let Some(cached_results) = self.get_from_cache(&cache_key) { if let Some(cached_results) = self.get_from_cache(&cache_key) {
return Ok(cached_results); return Ok(cached_results);
} }
// Initialize collections for results // Initialize collections for results
let mut found_files: Vec<FileDto> = Vec::new(); let mut found_files: Vec<FileDto> = Vec::new();
let mut found_folders: Vec<FolderDto> = Vec::new(); let mut found_folders: Vec<FolderDto> = Vec::new();
// Perform search in the specified folder or at the root // Perform search in the specified folder or at the root
self.search_recursive( self.search_recursive(
criteria.folder_id.as_deref(), criteria.folder_id.as_deref(),
&criteria, &criteria,
&mut found_files, &mut found_files,
&mut found_folders, &mut found_folders,
).await?; )
.await?;
// Apply pagination // Apply pagination
let total_count = found_files.len() + found_folders.len(); let total_count = found_files.len() + found_folders.len();
// Sort by relevance or date according to criteria // Sort by relevance or date according to criteria
// By default, sort by modification date (most recent first) // By default, sort by modification date (most recent first)
found_files.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); found_files.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
found_folders.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); found_folders.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
// Apply limit and offset for pagination // Apply limit and offset for pagination
let start_idx = criteria.offset.min(total_count); let start_idx = criteria.offset.min(total_count);
let end_idx = (criteria.offset + criteria.limit).min(total_count); let end_idx = (criteria.offset + criteria.limit).min(total_count);
let paginated_items: Vec<(bool, usize)> = (start_idx..end_idx) let paginated_items: Vec<(bool, usize)> = (start_idx..end_idx)
.map(|i| { .map(|i| {
if i < found_folders.len() { if i < found_folders.len() {
@@ -414,11 +442,11 @@ impl SearchUseCase for SearchService {
} }
}) })
.collect(); .collect();
// Extract paginated items // Extract paginated items
let mut paginated_folders = Vec::new(); let mut paginated_folders = Vec::new();
let mut paginated_files = Vec::new(); let mut paginated_files = Vec::new();
for (is_folder, idx) in paginated_items { for (is_folder, idx) in paginated_items {
if is_folder { if is_folder {
if idx < found_folders.len() { if idx < found_folders.len() {
@@ -428,7 +456,7 @@ impl SearchUseCase for SearchService {
paginated_files.push(found_files[idx].clone()); paginated_files.push(found_files[idx].clone());
} }
} }
// Create results object // Create results object
let search_results = SearchResultsDto::new( let search_results = SearchResultsDto::new(
paginated_files, paginated_files,
@@ -437,16 +465,16 @@ impl SearchUseCase for SearchService {
criteria.offset, criteria.offset,
Some(total_count), Some(total_count),
); );
// Store in cache // Store in cache
self.store_in_cache(cache_key, search_results.clone()); self.store_in_cache(cache_key, search_results.clone());
Ok(search_results) Ok(search_results)
} }
/** /**
* Clears the search results cache. * Clears the search results cache.
* *
* @return Result indicating success * @return Result indicating success
*/ */
async fn clear_search_cache(&self) -> Result<()> { async fn clear_search_cache(&self) -> Result<()> {
@@ -462,18 +490,18 @@ impl SearchService {
/// Creates a stub version of the service for testing /// Creates a stub version of the service for testing
pub fn new_stub() -> impl SearchUseCase { pub fn new_stub() -> impl SearchUseCase {
struct SearchServiceStub; struct SearchServiceStub;
#[async_trait] #[async_trait]
impl SearchUseCase for SearchServiceStub { impl SearchUseCase for SearchServiceStub {
async fn search(&self, _criteria: SearchCriteriaDto) -> Result<SearchResultsDto> { async fn search(&self, _criteria: SearchCriteriaDto) -> Result<SearchResultsDto> {
Ok(SearchResultsDto::empty()) Ok(SearchResultsDto::empty())
} }
async fn clear_search_cache(&self) -> Result<()> { async fn clear_search_cache(&self) -> Result<()> {
Ok(()) Ok(())
} }
} }
SearchServiceStub SearchServiceStub
} }
} }
+177 -89
View File
@@ -47,7 +47,9 @@ impl From<ShareServiceError> for DomainError {
ShareServiceError::ItemNotFound(s) => DomainError::not_found("Item", s), ShareServiceError::ItemNotFound(s) => DomainError::not_found("Item", s),
ShareServiceError::AccessDenied(s) => DomainError::access_denied("Share", s), ShareServiceError::AccessDenied(s) => DomainError::access_denied("Share", s),
ShareServiceError::InvalidPassword(s) => DomainError::access_denied("Share", s), ShareServiceError::InvalidPassword(s) => DomainError::access_denied("Share", s),
ShareServiceError::Expired => DomainError::access_denied("Share", "Share has expired".to_string()), ShareServiceError::Expired => {
DomainError::access_denied("Share", "Share has expired".to_string())
}
ShareServiceError::Repository(s) => DomainError::internal_error("Share", s), ShareServiceError::Repository(s) => DomainError::internal_error("Share", s),
ShareServiceError::InvalidItemType(s) => DomainError::validation_error(s), ShareServiceError::InvalidItemType(s) => DomainError::validation_error(s),
ShareServiceError::Validation(s) => DomainError::validation_error(s), ShareServiceError::Validation(s) => DomainError::validation_error(s),
@@ -91,13 +93,23 @@ impl ShareService {
self.file_repository self.file_repository
.get_file(item_id) // Using the correct method from the FileStoragePort trait .get_file(item_id) // Using the correct method from the FileStoragePort trait
.await .await
.map_err(|_| ShareServiceError::ItemNotFound(format!("File with ID {} not found", item_id)))?; .map_err(|_| {
ShareServiceError::ItemNotFound(format!(
"File with ID {} not found",
item_id
))
})?;
} }
ShareItemType::Folder => { ShareItemType::Folder => {
self.folder_repository self.folder_repository
.get_folder(item_id) // Using the correct method from the FolderStoragePort trait .get_folder(item_id) // Using the correct method from the FolderStoragePort trait
.await .await
.map_err(|_| ShareServiceError::ItemNotFound(format!("Folder with ID {} not found", item_id)))?; .map_err(|_| {
ShareServiceError::ItemNotFound(format!(
"Folder with ID {} not found",
item_id
))
})?;
} }
} }
Ok(()) Ok(())
@@ -105,13 +117,13 @@ impl ShareService {
/// Password hash using Argon2id (resistant to timing attacks and GPU attacks) /// Password hash using Argon2id (resistant to timing attacks and GPU attacks)
fn hash_password(&self, password: &str) -> String { fn hash_password(&self, password: &str) -> String {
use argon2::{Argon2, PasswordHasher};
use argon2::password_hash::SaltString; use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHasher};
use rand_core::OsRng; use rand_core::OsRng;
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default(); let argon2 = Argon2::default();
argon2 argon2
.hash_password(password.as_bytes(), &salt) .hash_password(password.as_bytes(), &salt)
.expect("Failed to hash share password") .expect("Failed to hash share password")
@@ -167,7 +179,9 @@ impl ShareUseCase for ShareService {
.share_repository .share_repository
.find_share_by_id(id) .find_share_by_id(id)
.await .await
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?; .map_err(|e| {
ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e))
})?;
// Check if it has expired // Check if it has expired
if share.is_expired() { if share.is_expired() {
@@ -184,7 +198,9 @@ impl ShareUseCase for ShareService {
.share_repository .share_repository
.find_share_by_token(token) .find_share_by_token(token)
.await .await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?; .map_err(|e| {
ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e))
})?;
// Check if it has expired // Check if it has expired
if share.is_expired() { if share.is_expired() {
@@ -229,7 +245,9 @@ impl ShareUseCase for ShareService {
.share_repository .share_repository
.find_share_by_id(id) .find_share_by_id(id)
.await .await
.map_err(|e| ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e)))?; .map_err(|e| {
ShareServiceError::NotFound(format!("Share with ID {} not found: {}", id, e))
})?;
// Update permissions if provided // Update permissions if provided
if let Some(permissions_dto) = dto.permissions { if let Some(permissions_dto) = dto.permissions {
@@ -264,7 +282,10 @@ impl ShareUseCase for ShareService {
.map_err(|e| ShareServiceError::Repository(e.to_string()))?; .map_err(|e| ShareServiceError::Repository(e.to_string()))?;
// Convert the entity to DTO for the response // Convert the entity to DTO for the response
Ok(ShareDto::from_entity(&updated_share, &self.config.base_url())) Ok(ShareDto::from_entity(
&updated_share,
&self.config.base_url(),
))
} }
async fn delete_shared_link(&self, id: &str) -> Result<(), DomainError> { async fn delete_shared_link(&self, id: &str) -> Result<(), DomainError> {
@@ -300,12 +321,7 @@ impl ShareUseCase for ShareService {
.collect(); .collect();
// Create the paginated result // Create the paginated result
let paginated = PaginatedResponseDto::new( let paginated = PaginatedResponseDto::new(share_dtos, page, per_page, total);
share_dtos,
page,
per_page,
total
);
Ok(paginated) Ok(paginated)
} }
@@ -320,7 +336,9 @@ impl ShareUseCase for ShareService {
.share_repository .share_repository
.find_share_by_token(token) .find_share_by_token(token)
.await .await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?; .map_err(|e| {
ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e))
})?;
// Check if it has expired // Check if it has expired
if share.is_expired() { if share.is_expired() {
@@ -329,9 +347,7 @@ impl ShareUseCase for ShareService {
// Verify the password using the infrastructure port // Verify the password using the infrastructure port
match share.password_hash() { match share.password_hash() {
Some(hash) => { Some(hash) => self.password_hasher.verify_password(password, hash),
self.password_hasher.verify_password(password, hash)
}
None => Ok(true), // No password required None => Ok(true), // No password required
} }
} }
@@ -342,7 +358,9 @@ impl ShareUseCase for ShareService {
.share_repository .share_repository
.find_share_by_token(token) .find_share_by_token(token)
.await .await
.map_err(|e| ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e)))?; .map_err(|e| {
ShareServiceError::NotFound(format!("Share with token {} not found: {}", token, e))
})?;
// Check if it has expired // Check if it has expired
if share.is_expired() { if share.is_expired() {
@@ -365,9 +383,9 @@ impl ShareUseCase for ShareService {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::application::ports::share_ports::ShareStoragePort;
use crate::application::ports::auth_ports::PasswordHasherPort;
use crate::application::dtos::share_dto::SharePermissionsDto; use crate::application::dtos::share_dto::SharePermissionsDto;
use crate::application::ports::auth_ports::PasswordHasherPort;
use crate::application::ports::share_ports::ShareStoragePort;
use crate::common::config::AppConfig; use crate::common::config::AppConfig;
use crate::domain::repositories::folder_repository::FolderRepository; use crate::domain::repositories::folder_repository::FolderRepository;
use async_trait::async_trait; use async_trait::async_trait;
@@ -391,12 +409,17 @@ mod tests {
#[async_trait] #[async_trait]
impl FileReadPort for MockFileRepository { impl FileReadPort for MockFileRepository {
async fn get_file(&self, id: &str) -> Result<crate::domain::entities::file::File, DomainError> { async fn get_file(
&self,
id: &str,
) -> Result<crate::domain::entities::file::File, DomainError> {
if id == "test_file_id" { if id == "test_file_id" {
let file = crate::domain::entities::file::File::new( let file = crate::domain::entities::file::File::new(
id.to_string(), id.to_string(),
"test.txt".to_string(), "test.txt".to_string(),
crate::domain::services::path_service::StoragePath::from_string("/path/to/test.txt"), crate::domain::services::path_service::StoragePath::from_string(
"/path/to/test.txt",
),
123, 123,
"text/plain".to_string(), "text/plain".to_string(),
None, None,
@@ -407,11 +430,14 @@ mod tests {
Err(DomainError::not_found("File", id)) Err(DomainError::not_found("File", id))
} }
} }
async fn list_files(&self, _folder_id: Option<&str>) -> Result<Vec<crate::domain::entities::file::File>, DomainError> { async fn list_files(
&self,
_folder_id: Option<&str>,
) -> Result<Vec<crate::domain::entities::file::File>, DomainError> {
unimplemented!() unimplemented!()
} }
async fn get_file_content(&self, _id: &str) -> Result<Vec<u8>, DomainError> { async fn get_file_content(&self, _id: &str) -> Result<Vec<u8>, DomainError> {
unimplemented!() unimplemented!()
} }
@@ -419,7 +445,10 @@ mod tests {
async fn get_file_stream( async fn get_file_stream(
&self, &self,
_id: &str, _id: &str,
) -> Result<Box<dyn futures::Stream<Item = Result<bytes::Bytes, std::io::Error>> + Send>, DomainError> { ) -> Result<
Box<dyn futures::Stream<Item = Result<bytes::Bytes, std::io::Error>> + Send>,
DomainError,
> {
unimplemented!() unimplemented!()
} }
@@ -428,7 +457,10 @@ mod tests {
_id: &str, _id: &str,
_start: u64, _start: u64,
_end: Option<u64>, _end: Option<u64>,
) -> Result<Box<dyn futures::Stream<Item = Result<bytes::Bytes, std::io::Error>> + Send>, DomainError> { ) -> Result<
Box<dyn futures::Stream<Item = Result<bytes::Bytes, std::io::Error>> + Send>,
DomainError,
> {
unimplemented!() unimplemented!()
} }
@@ -436,7 +468,10 @@ mod tests {
unimplemented!() unimplemented!()
} }
async fn get_file_path(&self, _id: &str) -> Result<crate::domain::services::path_service::StoragePath, DomainError> { async fn get_file_path(
&self,
_id: &str,
) -> Result<crate::domain::services::path_service::StoragePath, DomainError> {
unimplemented!() unimplemented!()
} }
@@ -447,16 +482,25 @@ mod tests {
#[async_trait] #[async_trait]
impl FolderRepository for MockFolderRepository { impl FolderRepository for MockFolderRepository {
async fn create_folder(&self, _name: String, _parent_id: Option<String>) -> Result<crate::domain::entities::folder::Folder, DomainError> { async fn create_folder(
&self,
_name: String,
_parent_id: Option<String>,
) -> Result<crate::domain::entities::folder::Folder, DomainError> {
unimplemented!() unimplemented!()
} }
async fn get_folder(&self, id: &str) -> Result<crate::domain::entities::folder::Folder, DomainError> { async fn get_folder(
&self,
id: &str,
) -> Result<crate::domain::entities::folder::Folder, DomainError> {
if id == "test_folder_id" { if id == "test_folder_id" {
let folder = crate::domain::entities::folder::Folder::new( let folder = crate::domain::entities::folder::Folder::new(
id.to_string(), id.to_string(),
"test".to_string(), "test".to_string(),
crate::domain::services::path_service::StoragePath::from_string("/path/to/test"), crate::domain::services::path_service::StoragePath::from_string(
"/path/to/test",
),
None, None,
) )
.unwrap(); .unwrap();
@@ -466,35 +510,62 @@ mod tests {
} }
} }
async fn get_folder_by_path(&self, _storage_path: &crate::domain::services::path_service::StoragePath) -> Result<crate::domain::entities::folder::Folder, DomainError> { async fn get_folder_by_path(
unimplemented!() &self,
} _storage_path: &crate::domain::services::path_service::StoragePath,
) -> Result<crate::domain::entities::folder::Folder, DomainError> {
async fn list_folders(&self, _parent_id: Option<&str>) -> Result<Vec<crate::domain::entities::folder::Folder>, DomainError> {
unimplemented!() unimplemented!()
} }
async fn list_folders_paginated(&self, _parent_id: Option<&str>, _offset: usize, _limit: usize, _include_total: bool) -> Result<(Vec<crate::domain::entities::folder::Folder>, Option<usize>), DomainError> { async fn list_folders(
&self,
_parent_id: Option<&str>,
) -> Result<Vec<crate::domain::entities::folder::Folder>, DomainError> {
unimplemented!() unimplemented!()
} }
async fn rename_folder(&self, _id: &str, _new_name: String) -> Result<crate::domain::entities::folder::Folder, DomainError> { async fn list_folders_paginated(
&self,
_parent_id: Option<&str>,
_offset: usize,
_limit: usize,
_include_total: bool,
) -> Result<(Vec<crate::domain::entities::folder::Folder>, Option<usize>), DomainError>
{
unimplemented!() unimplemented!()
} }
async fn move_folder(&self, _id: &str, _new_parent_id: Option<&str>) -> Result<crate::domain::entities::folder::Folder, DomainError> { async fn rename_folder(
&self,
_id: &str,
_new_name: String,
) -> Result<crate::domain::entities::folder::Folder, DomainError> {
unimplemented!() unimplemented!()
} }
async fn move_folder(
&self,
_id: &str,
_new_parent_id: Option<&str>,
) -> Result<crate::domain::entities::folder::Folder, DomainError> {
unimplemented!()
}
async fn delete_folder(&self, _id: &str) -> Result<(), DomainError> { async fn delete_folder(&self, _id: &str) -> Result<(), DomainError> {
unimplemented!() unimplemented!()
} }
async fn folder_exists(&self, _storage_path: &crate::domain::services::path_service::StoragePath) -> Result<bool, DomainError> { async fn folder_exists(
&self,
_storage_path: &crate::domain::services::path_service::StoragePath,
) -> Result<bool, DomainError> {
unimplemented!() unimplemented!()
} }
async fn get_folder_path(&self, _id: &str) -> Result<crate::domain::services::path_service::StoragePath, DomainError> { async fn get_folder_path(
&self,
_id: &str,
) -> Result<crate::domain::services::path_service::StoragePath, DomainError> {
unimplemented!() unimplemented!()
} }
@@ -502,7 +573,11 @@ mod tests {
unimplemented!() unimplemented!()
} }
async fn restore_from_trash(&self, _folder_id: &str, _original_path: &str) -> Result<(), DomainError> { async fn restore_from_trash(
&self,
_folder_id: &str,
_original_path: &str,
) -> Result<(), DomainError> {
unimplemented!() unimplemented!()
} }
@@ -530,91 +605,103 @@ mod tests {
async fn save_share(&self, share: &Share) -> Result<Share, DomainError> { async fn save_share(&self, share: &Share) -> Result<Share, DomainError> {
let mut shares = self.shares.lock().unwrap(); let mut shares = self.shares.lock().unwrap();
let mut tokens = self.tokens.lock().unwrap(); let mut tokens = self.tokens.lock().unwrap();
shares.insert(share.id().to_string(), share.clone()); shares.insert(share.id().to_string(), share.clone());
tokens.insert(share.token().to_string(), share.id().to_string()); tokens.insert(share.token().to_string(), share.id().to_string());
Ok(share.clone()) Ok(share.clone())
} }
async fn find_share_by_id(&self, id: &str) -> Result<Share, DomainError> { async fn find_share_by_id(&self, id: &str) -> Result<Share, DomainError> {
let shares = self.shares.lock().unwrap(); let shares = self.shares.lock().unwrap();
shares.get(id) shares
.get(id)
.cloned() .cloned()
.ok_or_else(|| DomainError::not_found("Share", id)) .ok_or_else(|| DomainError::not_found("Share", id))
} }
async fn find_share_by_token(&self, token: &str) -> Result<Share, DomainError> { async fn find_share_by_token(&self, token: &str) -> Result<Share, DomainError> {
let tokens = self.tokens.lock().unwrap(); let tokens = self.tokens.lock().unwrap();
let shares = self.shares.lock().unwrap(); let shares = self.shares.lock().unwrap();
let id = tokens.get(token) let id = tokens
.get(token)
.ok_or_else(|| DomainError::not_found("Share", token))?; .ok_or_else(|| DomainError::not_found("Share", token))?;
shares.get(id) shares
.get(id)
.cloned() .cloned()
.ok_or_else(|| DomainError::not_found("Share", id.as_str())) .ok_or_else(|| DomainError::not_found("Share", id.as_str()))
} }
async fn find_shares_by_item(&self, item_id: &str, item_type: &ShareItemType) -> Result<Vec<Share>, DomainError> { async fn find_shares_by_item(
&self,
item_id: &str,
item_type: &ShareItemType,
) -> Result<Vec<Share>, DomainError> {
let shares = self.shares.lock().unwrap(); let shares = self.shares.lock().unwrap();
let type_str = item_type.to_string(); let type_str = item_type.to_string();
let result: Vec<Share> = shares.values() let result: Vec<Share> = shares
.values()
.filter(|s| s.item_id() == item_id && s.item_type().to_string() == type_str) .filter(|s| s.item_id() == item_id && s.item_type().to_string() == type_str)
.cloned() .cloned()
.collect(); .collect();
Ok(result) Ok(result)
} }
async fn update_share(&self, share: &Share) -> Result<Share, DomainError> { async fn update_share(&self, share: &Share) -> Result<Share, DomainError> {
let mut shares = self.shares.lock().unwrap(); let mut shares = self.shares.lock().unwrap();
let id_str = share.id().to_string(); let id_str = share.id().to_string();
if !shares.contains_key(&id_str) { if !shares.contains_key(&id_str) {
return Err(DomainError::not_found("Share", &id_str)); return Err(DomainError::not_found("Share", &id_str));
} }
shares.insert(id_str, share.clone()); shares.insert(id_str, share.clone());
Ok(share.clone()) Ok(share.clone())
} }
async fn delete_share(&self, id: &str) -> Result<(), DomainError> { async fn delete_share(&self, id: &str) -> Result<(), DomainError> {
let mut shares = self.shares.lock().unwrap(); let mut shares = self.shares.lock().unwrap();
let mut tokens = self.tokens.lock().unwrap(); let mut tokens = self.tokens.lock().unwrap();
// Find the share to get the token // Find the share to get the token
let share = shares.get(id) let share = shares
.get(id)
.ok_or_else(|| DomainError::not_found("Share", id))?; .ok_or_else(|| DomainError::not_found("Share", id))?;
// Remove token mapping // Remove token mapping
tokens.remove(share.token()); tokens.remove(share.token());
// Remove the share // Remove the share
shares.remove(id); shares.remove(id);
Ok(()) Ok(())
} }
async fn find_shares_by_user(&self, user_id: &str, offset: usize, limit: usize) -> Result<(Vec<Share>, usize), DomainError> { async fn find_shares_by_user(
&self,
user_id: &str,
offset: usize,
limit: usize,
) -> Result<(Vec<Share>, usize), DomainError> {
let shares = self.shares.lock().unwrap(); let shares = self.shares.lock().unwrap();
let user_shares: Vec<Share> = shares.values() let user_shares: Vec<Share> = shares
.values()
.filter(|s| s.created_by() == user_id) .filter(|s| s.created_by() == user_id)
.cloned() .cloned()
.collect(); .collect();
let total = user_shares.len(); let total = user_shares.len();
// Apply pagination // Apply pagination
let paginated = user_shares.into_iter() let paginated = user_shares.into_iter().skip(offset).take(limit).collect();
.skip(offset)
.take(limit)
.collect();
Ok((paginated, total)) Ok((paginated, total))
} }
} }
@@ -622,14 +709,15 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_create_shared_link() { async fn test_create_shared_link() {
let config = Arc::new(AppConfig::default()); let config = Arc::new(AppConfig::default());
let share_repo = Arc::new(MockShareRepository::new()); let share_repo = Arc::new(MockShareRepository::new());
let file_repo = Arc::new(MockFileRepository); let file_repo = Arc::new(MockFileRepository);
let folder_repo = Arc::new(MockFolderRepository); let folder_repo = Arc::new(MockFolderRepository);
let password_hasher = Arc::new(MockPasswordHasher); let password_hasher = Arc::new(MockPasswordHasher);
let service = ShareService::new(config, share_repo, file_repo, folder_repo, password_hasher); let service =
ShareService::new(config, share_repo, file_repo, folder_repo, password_hasher);
// Test creating a file share // Test creating a file share
let dto = CreateShareDto { let dto = CreateShareDto {
item_id: "test_file_id".to_string(), item_id: "test_file_id".to_string(),
@@ -642,14 +730,14 @@ mod tests {
reshare: false, reshare: false,
}), }),
}; };
let result = service.create_shared_link("user123", dto).await; let result = service.create_shared_link("user123", dto).await;
assert!(result.is_ok()); assert!(result.is_ok());
let share_dto = result.unwrap(); let share_dto = result.unwrap();
assert_eq!(share_dto.item_id, "test_file_id"); assert_eq!(share_dto.item_id, "test_file_id");
assert_eq!(share_dto.item_type, "file"); assert_eq!(share_dto.item_type, "file");
assert!(share_dto.has_password); assert!(share_dto.has_password);
assert!(share_dto.url.starts_with("http://127.0.0.1:8085/s/")); assert!(share_dto.url.starts_with("http://127.0.0.1:8085/s/"));
} }
} }
+143 -84
View File
@@ -1,30 +1,30 @@
use async_trait::async_trait;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
use thiserror::Error; use thiserror::Error;
use crate::application::ports::outbound::{FolderStoragePort, IdMappingPort, StoragePort};
use crate::domain::entities::folder::Folder; use crate::domain::entities::folder::Folder;
use crate::domain::services::path_service::StoragePath; use crate::domain::services::path_service::StoragePath;
use crate::application::ports::outbound::{IdMappingPort, StoragePort, FolderStoragePort};
/// Storage mediator specific errors /// Storage mediator specific errors
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum StorageMediatorError { pub enum StorageMediatorError {
#[error("Entity not found: {0}")] #[error("Entity not found: {0}")]
NotFound(String), NotFound(String),
#[error("Entity already exists: {0}")] #[error("Entity already exists: {0}")]
AlreadyExists(String), AlreadyExists(String),
#[error("Invalid path: {0}")] #[error("Invalid path: {0}")]
InvalidPath(String), InvalidPath(String),
#[error("Access error: {0}")] #[error("Access error: {0}")]
AccessError(String), AccessError(String),
#[error("Internal error: {0}")] #[error("Internal error: {0}")]
InternalError(String), InternalError(String),
#[error("Domain error: {0}")] #[error("Domain error: {0}")]
DomainError(#[from] crate::common::errors::DomainError), DomainError(#[from] crate::common::errors::DomainError),
} }
@@ -37,36 +37,45 @@ pub type StorageMediatorResult<T> = Result<T, StorageMediatorError>;
pub trait StorageMediator: Send + Sync + 'static { pub trait StorageMediator: Send + Sync + 'static {
/// Gets the path of a folder by its ID /// Gets the path of a folder by its ID
async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf>; async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf>;
/// Gets the domain path of a folder by its ID /// Gets the domain path of a folder by its ID
async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath>; async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath>;
/// Gets all details of a folder by its ID /// Gets all details of a folder by its ID
async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder>; async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder>;
/// Checks if a file exists at a specific path /// Checks if a file exists at a specific path
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>; async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
/// Checks if a file exists at a specific domain path /// Checks if a file exists at a specific domain path
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>; async fn file_exists_at_storage_path(
&self,
storage_path: &StoragePath,
) -> StorageMediatorResult<bool>;
/// Checks if a folder exists at a specific path /// Checks if a folder exists at a specific path
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>; async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
/// Checks if a folder exists at a specific domain path /// Checks if a folder exists at a specific domain path
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool>; async fn folder_exists_at_storage_path(
&self,
storage_path: &StoragePath,
) -> StorageMediatorResult<bool>;
/// Resolves a relative path to absolute (legacy) /// Resolves a relative path to absolute (legacy)
fn resolve_path(&self, relative_path: &Path) -> PathBuf; fn resolve_path(&self, relative_path: &Path) -> PathBuf;
/// Resolves a domain path to an absolute physical path /// Resolves a domain path to an absolute physical path
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf; fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf;
/// Creates a directory if it does not exist (legacy) /// Creates a directory if it does not exist (legacy)
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()>; async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()>;
/// Creates a directory if it does not exist /// Creates a directory if it does not exist
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()>; async fn ensure_storage_directory(
&self,
storage_path: &StoragePath,
) -> StorageMediatorResult<()>;
} }
/// Concrete implementation of the storage mediator /// Concrete implementation of the storage mediator
@@ -77,10 +86,18 @@ pub struct FileSystemStorageMediator {
} }
impl FileSystemStorageMediator { impl FileSystemStorageMediator {
pub fn new(folder_storage_port: Arc<dyn FolderStoragePort>, path_service: Arc<dyn StoragePort>, id_mapping: Arc<dyn IdMappingPort>) -> Self { pub fn new(
Self { folder_storage_port, path_service, id_mapping } folder_storage_port: Arc<dyn FolderStoragePort>,
path_service: Arc<dyn StoragePort>,
id_mapping: Arc<dyn IdMappingPort>,
) -> Self {
Self {
folder_storage_port,
path_service,
id_mapping,
}
} }
/// Creates a stub implementation for initialization bootstrapping /// Creates a stub implementation for initialization bootstrapping
pub fn new_stub() -> StubStorageMediator { pub fn new_stub() -> StubStorageMediator {
StubStorageMediator::new() StubStorageMediator::new()
@@ -109,46 +126,60 @@ impl StorageMediator for StubStorageMediator {
// Return a stub path // Return a stub path
Ok(PathBuf::from("/tmp")) Ok(PathBuf::from("/tmp"))
} }
async fn get_folder_storage_path(&self, _folder_id: &str) -> StorageMediatorResult<StoragePath> { async fn get_folder_storage_path(
&self,
_folder_id: &str,
) -> StorageMediatorResult<StoragePath> {
// Return a stub storage path // Return a stub storage path
Ok(StoragePath::root()) Ok(StoragePath::root())
} }
async fn get_folder(&self, _folder_id: &str) -> StorageMediatorResult<Folder> { async fn get_folder(&self, _folder_id: &str) -> StorageMediatorResult<Folder> {
// This is a stub that should never be called during initialization // This is a stub that should never be called during initialization
Err(StorageMediatorError::NotFound("Stub not implemented".to_string())) Err(StorageMediatorError::NotFound(
"Stub not implemented".to_string(),
))
} }
async fn file_exists_at_path(&self, _path: &Path) -> StorageMediatorResult<bool> { async fn file_exists_at_path(&self, _path: &Path) -> StorageMediatorResult<bool> {
Ok(false) Ok(false)
} }
async fn file_exists_at_storage_path(&self, _storage_path: &StoragePath) -> StorageMediatorResult<bool> { async fn file_exists_at_storage_path(
&self,
_storage_path: &StoragePath,
) -> StorageMediatorResult<bool> {
Ok(false) Ok(false)
} }
async fn folder_exists_at_path(&self, _path: &Path) -> StorageMediatorResult<bool> { async fn folder_exists_at_path(&self, _path: &Path) -> StorageMediatorResult<bool> {
Ok(false) Ok(false)
} }
async fn folder_exists_at_storage_path(&self, _storage_path: &StoragePath) -> StorageMediatorResult<bool> { async fn folder_exists_at_storage_path(
&self,
_storage_path: &StoragePath,
) -> StorageMediatorResult<bool> {
Ok(false) Ok(false)
} }
fn resolve_path(&self, _relative_path: &Path) -> PathBuf { fn resolve_path(&self, _relative_path: &Path) -> PathBuf {
PathBuf::from("/tmp") PathBuf::from("/tmp")
} }
fn resolve_storage_path(&self, _storage_path: &StoragePath) -> PathBuf { fn resolve_storage_path(&self, _storage_path: &StoragePath) -> PathBuf {
PathBuf::from("/tmp") PathBuf::from("/tmp")
} }
async fn ensure_directory(&self, _path: &Path) -> StorageMediatorResult<()> { async fn ensure_directory(&self, _path: &Path) -> StorageMediatorResult<()> {
Ok(()) Ok(())
} }
async fn ensure_storage_directory(&self, _storage_path: &StoragePath) -> StorageMediatorResult<()> { async fn ensure_storage_directory(
&self,
_storage_path: &StoragePath,
) -> StorageMediatorResult<()> {
Ok(()) Ok(())
} }
} }
@@ -156,112 +187,140 @@ impl StorageMediator for StubStorageMediator {
#[async_trait] #[async_trait]
impl StorageMediator for FileSystemStorageMediator { impl StorageMediator for FileSystemStorageMediator {
async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf> { async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf> {
let folder = self.folder_storage_port.get_folder(folder_id).await let folder = self
.folder_storage_port
.get_folder(folder_id)
.await
.map_err(StorageMediatorError::from)?; .map_err(StorageMediatorError::from)?;
// Need to get the path from folder ID // Need to get the path from folder ID
let storage_path = self.id_mapping.get_path_by_id(folder.id()).await let storage_path = self
.id_mapping
.get_path_by_id(folder.id())
.await
.map_err(StorageMediatorError::from)?; .map_err(StorageMediatorError::from)?;
// Convert StoragePath to PathBuf // Convert StoragePath to PathBuf
let path_buf = self.path_service.resolve_path(&storage_path); let path_buf = self.path_service.resolve_path(&storage_path);
Ok(path_buf) Ok(path_buf)
} }
async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath> { async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath> {
let folder = self.folder_storage_port.get_folder(folder_id).await let folder = self
.folder_storage_port
.get_folder(folder_id)
.await
.map_err(StorageMediatorError::from)?; .map_err(StorageMediatorError::from)?;
// Get path by folder ID - will already be a StoragePath // Get path by folder ID - will already be a StoragePath
let storage_path = self.id_mapping.get_path_by_id(folder.id()).await let storage_path = self
.id_mapping
.get_path_by_id(folder.id())
.await
.map_err(StorageMediatorError::from)?; .map_err(StorageMediatorError::from)?;
Ok(storage_path) Ok(storage_path)
} }
async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder> { async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder> {
let folder = self.folder_storage_port.get_folder(folder_id).await let folder = self
.folder_storage_port
.get_folder(folder_id)
.await
.map_err(StorageMediatorError::from)?; .map_err(StorageMediatorError::from)?;
Ok(folder) Ok(folder)
} }
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> { async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_path(path); let abs_path = self.resolve_path(path);
// Check if it exists as a file (not as a directory) // Check if it exists as a file (not as a directory)
let exists = abs_path.exists() && abs_path.is_file(); let exists = abs_path.exists() && abs_path.is_file();
Ok(exists) Ok(exists)
} }
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> { async fn file_exists_at_storage_path(
&self,
storage_path: &StoragePath,
) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_storage_path(storage_path); let abs_path = self.resolve_storage_path(storage_path);
// Check if it exists as a file (not as a directory) // Check if it exists as a file (not as a directory)
let exists = abs_path.exists() && abs_path.is_file(); let exists = abs_path.exists() && abs_path.is_file();
Ok(exists) Ok(exists)
} }
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> { async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_path(path); let abs_path = self.resolve_path(path);
// Check if it exists as a directory // Check if it exists as a directory
let exists = abs_path.exists() && abs_path.is_dir(); let exists = abs_path.exists() && abs_path.is_dir();
Ok(exists) Ok(exists)
} }
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> { async fn folder_exists_at_storage_path(
&self,
storage_path: &StoragePath,
) -> StorageMediatorResult<bool> {
let abs_path = self.resolve_storage_path(storage_path); let abs_path = self.resolve_storage_path(storage_path);
// Check if it exists as a directory // Check if it exists as a directory
let exists = abs_path.exists() && abs_path.is_dir(); let exists = abs_path.exists() && abs_path.is_dir();
Ok(exists) Ok(exists)
} }
fn resolve_path(&self, relative_path: &Path) -> PathBuf { fn resolve_path(&self, relative_path: &Path) -> PathBuf {
// Legacy method using PathBuf // Legacy method using PathBuf
let path_str = relative_path.to_string_lossy().to_string(); let path_str = relative_path.to_string_lossy().to_string();
let storage_path = StoragePath::from_string(&path_str); let storage_path = StoragePath::from_string(&path_str);
self.path_service.resolve_path(&storage_path) self.path_service.resolve_path(&storage_path)
} }
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf { fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
self.path_service.resolve_path(storage_path) self.path_service.resolve_path(storage_path)
} }
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> { async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
let abs_path = self.resolve_path(path); let abs_path = self.resolve_path(path);
// Create directories if they don't exist // Create directories if they don't exist
if !abs_path.exists() { if !abs_path.exists() {
tokio::fs::create_dir_all(&abs_path).await tokio::fs::create_dir_all(&abs_path).await.map_err(|e| {
.map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?; StorageMediatorError::AccessError(format!("Could not create directory: {}", e))
})?;
} else if !abs_path.is_dir() { } else if !abs_path.is_dir() {
return Err(StorageMediatorError::InvalidPath( return Err(StorageMediatorError::InvalidPath(format!(
format!("Path exists but is not a directory: {}", abs_path.display()) "Path exists but is not a directory: {}",
)); abs_path.display()
)));
} }
Ok(()) Ok(())
} }
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> { async fn ensure_storage_directory(
&self,
storage_path: &StoragePath,
) -> StorageMediatorResult<()> {
let abs_path = self.resolve_storage_path(storage_path); let abs_path = self.resolve_storage_path(storage_path);
// Create directories if they don't exist // Create directories if they don't exist
if !abs_path.exists() { if !abs_path.exists() {
tokio::fs::create_dir_all(&abs_path).await tokio::fs::create_dir_all(&abs_path).await.map_err(|e| {
.map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?; StorageMediatorError::AccessError(format!("Could not create directory: {}", e))
})?;
} else if !abs_path.is_dir() { } else if !abs_path.is_dir() {
return Err(StorageMediatorError::InvalidPath( return Err(StorageMediatorError::InvalidPath(format!(
format!("Path exists but is not a directory: {}", abs_path.display()) "Path exists but is not a directory: {}",
)); abs_path.display()
)));
} }
Ok(()) Ok(())
} }
} }
@@ -1,14 +1,14 @@
use std::sync::Arc;
use async_trait::async_trait;
use tokio::task;
use crate::common::errors::DomainError;
use crate::application::ports::auth_ports::UserStoragePort; use crate::application::ports::auth_ports::UserStoragePort;
use crate::application::ports::storage_ports::{FileReadPort, StorageUsagePort}; use crate::application::ports::storage_ports::{FileReadPort, StorageUsagePort};
use tracing::{info, error, debug}; use crate::common::errors::DomainError;
use async_trait::async_trait;
use std::sync::Arc;
use tokio::task;
use tracing::{debug, error, info};
/** /**
* Service for managing and updating user storage usage statistics. * Service for managing and updating user storage usage statistics.
* *
* This service is responsible for calculating how much storage each user * This service is responsible for calculating how much storage each user
* is using and updating this information in the user records. * is using and updating this information in the user records.
*/ */
@@ -28,51 +28,63 @@ impl StorageUsageService {
user_repository, user_repository,
} }
} }
/// Calculates and updates storage usage for a specific user /// Calculates and updates storage usage for a specific user
pub async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> { pub async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> {
info!("Updating storage usage for user: {}", user_id); info!("Updating storage usage for user: {}", user_id);
// Get user's home folder pattern // Get user's home folder pattern
let user = self.user_repository.get_user_by_id(user_id).await?; let user = self.user_repository.get_user_by_id(user_id).await?;
let username = user.username(); let username = user.username();
// Calculate storage usage for this user // Calculate storage usage for this user
let total_usage = self.calculate_user_storage_usage(username).await?; let total_usage = self.calculate_user_storage_usage(username).await?;
// Update the user's storage usage in the database // Update the user's storage usage in the database
self.user_repository.update_storage_usage(user_id, total_usage).await?; self.user_repository
.update_storage_usage(user_id, total_usage)
info!("Updated storage usage for user {} to {} bytes", user_id, total_usage); .await?;
info!(
"Updated storage usage for user {} to {} bytes",
user_id, total_usage
);
Ok(total_usage) Ok(total_usage)
} }
/// Calculates a user's storage usage based on their home folder /// Calculates a user's storage usage based on their home folder
async fn calculate_user_storage_usage(&self, username: &str) -> Result<i64, DomainError> { async fn calculate_user_storage_usage(&self, username: &str) -> Result<i64, DomainError> {
debug!("Calculating storage for user: {}", username); debug!("Calculating storage for user: {}", username);
// First, try to find the user's home folder // First, try to find the user's home folder
// List all folders to locate the user's folder // List all folders to locate the user's folder
let all_folders = self.file_repository.list_files(None).await let all_folders = self
.file_repository
.list_files(None)
.await
.map_err(|e| DomainError::internal_error("File repository", e.to_string()))?; .map_err(|e| DomainError::internal_error("File repository", e.to_string()))?;
// Find the user's home folder (named "My Folder - {username}") // Find the user's home folder (named "My Folder - {username}")
let home_folder_name = format!("My Folder - {}", username); let home_folder_name = format!("My Folder - {}", username);
debug!("Looking for home folder: {}", home_folder_name); debug!("Looking for home folder: {}", home_folder_name);
let mut total_usage: i64 = 0; let mut total_usage: i64 = 0;
let mut home_folder_id = None; let mut home_folder_id = None;
// Find the home folder ID // Find the home folder ID
for folder in &all_folders { for folder in &all_folders {
if folder.name() == home_folder_name { if folder.name() == home_folder_name {
home_folder_id = Some(folder.id().to_string()); home_folder_id = Some(folder.id().to_string());
debug!("Found home folder for user {}: ID={}", username, folder.id()); debug!(
"Found home folder for user {}: ID={}",
username,
folder.id()
);
break; break;
} }
} }
// If we found the home folder, calculate total size // If we found the home folder, calculate total size
if let Some(folder_id) = home_folder_id { if let Some(folder_id) = home_folder_id {
// Calculate recursively // Calculate recursively
@@ -81,10 +93,10 @@ impl StorageUsageService {
// If no home folder found, just return 0 // If no home folder found, just return 0
debug!("No home folder found for user: {}", username); debug!("No home folder found for user: {}", username);
} }
Ok(total_usage) Ok(total_usage)
} }
/// Recursively calculates the size of a folder and all its contents /// Recursively calculates the size of a folder and all its contents
async fn calculate_folder_size(&self, folder_id: &str) -> Result<i64, DomainError> { async fn calculate_folder_size(&self, folder_id: &str) -> Result<i64, DomainError> {
// Implementation with explicit boxing to handle recursion in async functions // Implementation with explicit boxing to handle recursion in async functions
@@ -93,11 +105,13 @@ impl StorageUsageService {
folder_id: &str, folder_id: &str,
) -> Result<i64, DomainError> { ) -> Result<i64, DomainError> {
let mut total_size: i64 = 0; let mut total_size: i64 = 0;
// Get files directly in this folder // Get files directly in this folder
let files = repo.list_files(Some(folder_id)).await let files = repo
.list_files(Some(folder_id))
.await
.map_err(|e| DomainError::internal_error("File repository", e.to_string()))?; .map_err(|e| DomainError::internal_error("File repository", e.to_string()))?;
// Sum the size of all files // Sum the size of all files
for file in &files { for file in &files {
// Skip subdirectories at this level - we'll process them separately // Skip subdirectories at this level - we'll process them separately
@@ -105,16 +119,20 @@ impl StorageUsageService {
// Recursively calculate subfolder size with explicit boxing // Recursively calculate subfolder size with explicit boxing
let subfolder_id = file.id().to_string(); // Create owned copy let subfolder_id = file.id().to_string(); // Create owned copy
let repo_clone = repo.clone(); // Clone the repository let repo_clone = repo.clone(); // Clone the repository
// Use Box::pin to handle recursive async call // Use Box::pin to handle recursive async call
let subfolder_size_future = Box::pin(inner_calculate_size(repo_clone, &subfolder_id)); let subfolder_size_future =
Box::pin(inner_calculate_size(repo_clone, &subfolder_id));
match subfolder_size_future.await { match subfolder_size_future.await {
Ok(size) => { Ok(size) => {
total_size += size; total_size += size;
}, }
Err(e) => { Err(e) => {
error!("Error calculating size for subfolder {}: {}", subfolder_id, e); error!(
"Error calculating size for subfolder {}: {}",
subfolder_id, e
);
// Continue with other folders even if one fails // Continue with other folders even if one fails
} }
} }
@@ -123,10 +141,10 @@ impl StorageUsageService {
total_size += file.size() as i64; total_size += file.size() as i64;
} }
} }
Ok(total_size) Ok(total_size)
} }
// Start the calculation with a clone of our repository reference // Start the calculation with a clone of our repository reference
let repo_clone = Arc::clone(&self.file_repository); let repo_clone = Arc::clone(&self.file_repository);
inner_calculate_size(repo_clone, folder_id).await inner_calculate_size(repo_clone, folder_id).await
@@ -148,37 +166,40 @@ impl StorageUsagePort for StorageUsageService {
// Get the list of all users // Get the list of all users
let users = self.user_repository.list_users(1000, 0).await?; let users = self.user_repository.list_users(1000, 0).await?;
let mut update_tasks = Vec::new(); let mut update_tasks = Vec::new();
// Process users in parallel // Process users in parallel
for user in users { for user in users {
let user_id = user.id().to_string(); let user_id = user.id().to_string();
let service_clone = self.clone(); let service_clone = self.clone();
// Spawn a background task for each user // Spawn a background task for each user
let task = task::spawn(async move { let task = task::spawn(async move {
match service_clone.update_user_storage_usage(&user_id).await { match service_clone.update_user_storage_usage(&user_id).await {
Ok(usage) => { Ok(usage) => {
debug!("Updated storage usage for user {}: {} bytes", user_id, usage); debug!(
"Updated storage usage for user {}: {} bytes",
user_id, usage
);
Ok(()) Ok(())
}, }
Err(e) => { Err(e) => {
error!("Failed to update storage for user {}: {}", user_id, e); error!("Failed to update storage for user {}: {}", user_id, e);
Err(e) Err(e)
} }
} }
}); });
update_tasks.push(task); update_tasks.push(task);
} }
// Wait for all tasks to complete // Wait for all tasks to complete
for task in update_tasks { for task in update_tasks {
// We don't propagate errors from individual users to avoid failing the entire batch // We don't propagate errors from individual users to avoid failing the entire batch
let _ = task.await; let _ = task.await;
} }
info!("Completed batch update of all users' storage usage"); info!("Completed batch update of all users' storage usage");
Ok(()) Ok(())
} }
@@ -192,4 +213,4 @@ impl Clone for StorageUsageService {
user_repository: Arc::clone(&self.user_repository), user_repository: Arc::clone(&self.user_repository),
} }
} }
} }
+286 -141
View File
@@ -1,24 +1,24 @@
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use uuid::Uuid; use std::sync::Arc;
use tracing::{debug, error, info, instrument}; use tracing::{debug, error, info, instrument};
use uuid::Uuid;
use crate::application::dtos::trash_dto::TrashedItemDto; use crate::application::dtos::trash_dto::TrashedItemDto;
use crate::application::ports::trash_ports::TrashUseCase;
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::application::ports::outbound::FolderStoragePort; use crate::application::ports::outbound::FolderStoragePort;
use crate::common::errors::{Result, DomainError, ErrorKind}; use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::application::ports::trash_ports::TrashUseCase;
use crate::common::errors::{DomainError, ErrorKind, Result};
use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType}; use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
use crate::domain::repositories::trash_repository::TrashRepository; use crate::domain::repositories::trash_repository::TrashRepository;
/** /**
* Application service for trash operations. * Application service for trash operations.
* *
* The TrashService implements the trash management functionality in the application layer, * The TrashService implements the trash management functionality in the application layer,
* handling movement of files and folders to trash, restoration from trash, and permanent * handling movement of files and folders to trash, restoration from trash, and permanent
* deletion. It orchestrates interactions between the domain entities and infrastructure * deletion. It orchestrates interactions between the domain entities and infrastructure
* repositories while enforcing business rules like retention policies. * repositories while enforcing business rules like retention policies.
* *
* This service follows the Clean Architecture pattern by: * This service follows the Clean Architecture pattern by:
* - Depending on application ports rather than domain/infrastructure traits * - Depending on application ports rather than domain/infrastructure traits
* - Orchestrating domain operations without containing domain logic * - Orchestrating domain operations without containing domain logic
@@ -27,16 +27,16 @@ use crate::domain::repositories::trash_repository::TrashRepository;
pub struct TrashService { pub struct TrashService {
/// Repository for trash-specific operations like listing and retrieving trashed items /// Repository for trash-specific operations like listing and retrieving trashed items
trash_repository: Arc<dyn TrashRepository>, trash_repository: Arc<dyn TrashRepository>,
/// Port for file read operations (get file metadata) /// Port for file read operations (get file metadata)
file_read_port: Arc<dyn FileReadPort>, file_read_port: Arc<dyn FileReadPort>,
/// Port for file write operations (trash, restore, delete) /// Port for file write operations (trash, restore, delete)
file_write_port: Arc<dyn FileWritePort>, file_write_port: Arc<dyn FileWritePort>,
/// Port for folder operations (get folder, trash, restore, delete) /// Port for folder operations (get folder, trash, restore, delete)
folder_storage_port: Arc<dyn FolderStoragePort>, folder_storage_port: Arc<dyn FolderStoragePort>,
/// Number of days items should be kept in trash before automatic cleanup /// Number of days items should be kept in trash before automatic cleanup
retention_days: u32, retention_days: u32,
} }
@@ -62,7 +62,7 @@ impl TrashService {
fn to_dto(&self, item: TrashedItem) -> TrashedItemDto { fn to_dto(&self, item: TrashedItem) -> TrashedItemDto {
// Calculate days_until_deletion before moving item fields // Calculate days_until_deletion before moving item fields
let days_until_deletion = item.days_until_deletion(); let days_until_deletion = item.days_until_deletion();
TrashedItemDto { TrashedItemDto {
id: item.id().to_string(), id: item.id().to_string(),
original_id: item.original_id().to_string(), original_id: item.original_id().to_string(),
@@ -86,12 +86,18 @@ impl TrashService {
let user_uuid = Uuid::parse_str(user_id) let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
match self.trash_repository.get_trash_item(&item_uuid, &user_uuid).await? { match self
.trash_repository
.get_trash_item(&item_uuid, &user_uuid)
.await?
{
Some(item) => { Some(item) => {
if item.user_id() != user_uuid { if item.user_id() != user_uuid {
error!( error!(
"User {} attempted to access trash item {} owned by {}", "User {} attempted to access trash item {} owned by {}",
user_id, item_id, item.user_id() user_id,
item_id,
item.user_id()
); );
return Err(DomainError::access_denied( return Err(DomainError::access_denied(
"TrashItem", "TrashItem",
@@ -117,77 +123,84 @@ impl TrashUseCase for TrashService {
#[instrument(skip(self))] #[instrument(skip(self))]
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> { async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> {
debug!("Getting trash items for user: {}", user_id); debug!("Getting trash items for user: {}", user_id);
let user_uuid = Uuid::parse_str(user_id) let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
let items = self.trash_repository.get_trash_items(&user_uuid).await?; let items = self.trash_repository.get_trash_items(&user_uuid).await?;
let dtos = items.into_iter() let dtos = items.into_iter().map(|item| self.to_dto(item)).collect();
.map(|item| self.to_dto(item))
.collect();
Ok(dtos) Ok(dtos)
} }
#[instrument(skip(self))] #[instrument(skip(self))]
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()> { async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()> {
info!("Moving to trash: type={}, id={}, user={}", item_type, item_id, user_id); info!(
"Moving to trash: type={}, id={}, user={}",
item_type, item_id, user_id
);
debug!("User UUID validation: {}", user_id); debug!("User UUID validation: {}", user_id);
// Note: We do NOT call validate_user_ownership here because the item // Note: We do NOT call validate_user_ownership here because the item
// is not yet in the trash. Ownership validation is only for operations // is not yet in the trash. Ownership validation is only for operations
// on already-trashed items (restore, delete_permanently). // on already-trashed items (restore, delete_permanently).
// Parse UUIDs with detailed error handling // Parse UUIDs with detailed error handling
debug!("Validating item UUID: {}", item_id); debug!("Validating item UUID: {}", item_id);
let item_uuid = match Uuid::parse_str(item_id) { let item_uuid = match Uuid::parse_str(item_id) {
Ok(uuid) => { Ok(uuid) => {
debug!("Valid item UUID: {}", uuid); debug!("Valid item UUID: {}", uuid);
uuid uuid
}, }
Err(e) => { Err(e) => {
error!("Invalid item UUID: {} - Error: {}", item_id, e); error!("Invalid item UUID: {} - Error: {}", item_id, e);
return Err(DomainError::validation_error(format!("Invalid item ID: {}", e))); return Err(DomainError::validation_error(format!(
"Invalid item ID: {}",
e
)));
} }
}; };
debug!("Validating user UUID: {}", user_id); debug!("Validating user UUID: {}", user_id);
let user_uuid = match Uuid::parse_str(user_id) { let user_uuid = match Uuid::parse_str(user_id) {
Ok(uuid) => { Ok(uuid) => {
debug!("Valid user UUID: {}", uuid); debug!("Valid user UUID: {}", uuid);
uuid uuid
}, }
Err(e) => { Err(e) => {
error!("Invalid user UUID: {} - Error: {}", user_id, e); error!("Invalid user UUID: {} - Error: {}", user_id, e);
return Err(DomainError::validation_error(format!("Invalid user ID: {}", e))); return Err(DomainError::validation_error(format!(
"Invalid user ID: {}",
e
)));
} }
}; };
match item_type { match item_type {
"file" => { "file" => {
info!("Processing file to move to trash: {}", item_id); info!("Processing file to move to trash: {}", item_id);
// Get the file to verify it exists and capture its data // Get the file to verify it exists and capture its data
debug!("Getting file data: {}", item_id); debug!("Getting file data: {}", item_id);
let file = match self.file_read_port.get_file(item_id).await { let file = match self.file_read_port.get_file(item_id).await {
Ok(file) => { Ok(file) => {
debug!("File found: {} ({})", file.name(), item_id); debug!("File found: {} ({})", file.name(), item_id);
file file
}, }
Err(e) => { Err(e) => {
error!("Error getting file: {} - {}", item_id, e); error!("Error getting file: {} - {}", item_id, e);
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::NotFound, ErrorKind::NotFound,
"File", "File",
format!("Error retrieving file {}: {}", item_id, e) format!("Error retrieving file {}: {}", item_id, e),
)); ));
} }
}; };
let original_path = file.storage_path().to_string(); let original_path = file.storage_path().to_string();
debug!("Original file path: {}", original_path); debug!("Original file path: {}", original_path);
// Create the trash item // Create the trash item
debug!("Creating TrashedItem object for the file"); debug!("Creating TrashedItem object for the file");
let trashed_item = TrashedItem::new( let trashed_item = TrashedItem::new(
@@ -198,50 +211,62 @@ impl TrashUseCase for TrashService {
original_path, original_path,
self.retention_days, self.retention_days,
); );
debug!("TrashedItem created successfully: {} -> {}", file.name(), trashed_item.id()); debug!(
"TrashedItem created successfully: {} -> {}",
file.name(),
trashed_item.id()
);
// First add to trash index to register the item // First add to trash index to register the item
info!("Adding file {} to trash index", item_id); info!("Adding file {} to trash index", item_id);
match self.trash_repository.add_to_trash(&trashed_item).await { match self.trash_repository.add_to_trash(&trashed_item).await {
Ok(_) => { Ok(_) => {
debug!("File added to trash index successfully"); debug!("File added to trash index successfully");
}, }
Err(e) => { Err(e) => {
error!("Error adding file to trash index: {}", e); error!("Error adding file to trash index: {}", e);
return Err(DomainError::internal_error("TrashRepository", format!("Failed to add file to trash: {}", e))); return Err(DomainError::internal_error(
"TrashRepository",
format!("Failed to add file to trash: {}", e),
));
} }
}; };
// Then physically move the file to trash // Then physically move the file to trash
info!("Physically moving file to trash: {}", item_id); info!("Physically moving file to trash: {}", item_id);
match self.file_write_port.move_to_trash(item_id).await { match self.file_write_port.move_to_trash(item_id).await {
Ok(_) => { Ok(_) => {
debug!("File physically moved to trash successfully: {}", item_id); debug!("File physically moved to trash successfully: {}", item_id);
}, }
Err(e) => { Err(e) => {
error!("Error physically moving file to trash: {} - {}", item_id, e); error!("Error physically moving file to trash: {} - {}", item_id, e);
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"File", "File",
format!("Error moving file {} to trash: {}", item_id, e) format!("Error moving file {} to trash: {}", item_id, e),
)); ));
} }
} }
info!("File completely moved to trash: {}", item_id); info!("File completely moved to trash: {}", item_id);
Ok(()) Ok(())
}, }
"folder" => { "folder" => {
// Get the folder to verify it exists and capture its data // Get the folder to verify it exists and capture its data
let folder = self.folder_storage_port.get_folder(item_id).await let folder = self
.map_err(|e| DomainError::new( .folder_storage_port
ErrorKind::NotFound, .get_folder(item_id)
"Folder", .await
format!("Error retrieving folder {}: {}", item_id, e) .map_err(|e| {
))?; DomainError::new(
ErrorKind::NotFound,
"Folder",
format!("Error retrieving folder {}: {}", item_id, e),
)
})?;
let original_path = folder.storage_path().to_string(); let original_path = folder.storage_path().to_string();
// Create the trash item // Create the trash item
let trashed_item = TrashedItem::new( let trashed_item = TrashedItem::new(
item_uuid, item_uuid,
@@ -251,66 +276,89 @@ impl TrashUseCase for TrashService {
original_path, original_path,
self.retention_days, self.retention_days,
); );
// First add to trash index to register the item // First add to trash index to register the item
debug!("Adding folder {} to trash repository", item_id); debug!("Adding folder {} to trash repository", item_id);
match self.trash_repository.add_to_trash(&trashed_item).await { match self.trash_repository.add_to_trash(&trashed_item).await {
Ok(_) => debug!("Successfully added folder to trash repository"), Ok(_) => debug!("Successfully added folder to trash repository"),
Err(e) => { Err(e) => {
error!("Failed to add folder to trash repository: {}", e); error!("Failed to add folder to trash repository: {}", e);
return Err(DomainError::internal_error("TrashRepository", format!("Failed to add folder to trash: {}", e))); return Err(DomainError::internal_error(
"TrashRepository",
format!("Failed to add folder to trash: {}", e),
));
} }
}; };
// Then physically move the folder to trash // Then physically move the folder to trash
self.folder_storage_port.move_to_trash(item_id).await self.folder_storage_port
.map_err(|e| DomainError::new( .move_to_trash(item_id)
ErrorKind::InternalError, .await
"Folder", .map_err(|e| {
format!("Error moving folder {} to trash: {}", item_id, e) DomainError::new(
))?; ErrorKind::InternalError,
"Folder",
format!("Error moving folder {} to trash: {}", item_id, e),
)
})?;
debug!("Folder moved to trash: {}", item_id); debug!("Folder moved to trash: {}", item_id);
Ok(()) Ok(())
}, }
_ => Err(DomainError::validation_error(format!("Invalid item type: {}", item_type))), _ => Err(DomainError::validation_error(format!(
"Invalid item type: {}",
item_type
))),
} }
} }
#[instrument(skip(self))] #[instrument(skip(self))]
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> { async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> {
info!("Restoring item {} for user {}", trash_id, user_id); info!("Restoring item {} for user {}", trash_id, user_id);
let trash_uuid = match Uuid::parse_str(trash_id) { let trash_uuid = match Uuid::parse_str(trash_id) {
Ok(id) => { Ok(id) => {
info!("Trash UUID parsed successfully: {}", id); info!("Trash UUID parsed successfully: {}", id);
id id
}, }
Err(e) => { Err(e) => {
error!("Invalid trash ID format: {} - {}", trash_id, e); error!("Invalid trash ID format: {} - {}", trash_id, e);
return Err(DomainError::validation_error(format!("Invalid trash ID: {}", e))); return Err(DomainError::validation_error(format!(
"Invalid trash ID: {}",
e
)));
} }
}; };
let user_uuid = match Uuid::parse_str(user_id) { let user_uuid = match Uuid::parse_str(user_id) {
Ok(id) => { Ok(id) => {
info!("User UUID parsed successfully: {}", id); info!("User UUID parsed successfully: {}", id);
id id
}, }
Err(e) => { Err(e) => {
error!("Invalid user ID format: {} - {}", user_id, e); error!("Invalid user ID format: {} - {}", user_id, e);
return Err(DomainError::validation_error(format!("Invalid user ID: {}", e))); return Err(DomainError::validation_error(format!(
"Invalid user ID: {}",
e
)));
} }
}; };
// Get the trash item // Get the trash item
info!("Retrieving trash item from repository: ID={}", trash_id); info!("Retrieving trash item from repository: ID={}", trash_id);
let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await; let item_result = self
.trash_repository
.get_trash_item(&trash_uuid, &user_uuid)
.await;
match item_result { match item_result {
Ok(Some(item)) => { Ok(Some(item)) => {
info!("Found item in trash: ID={}, Type={:?}, OriginalID={}", info!(
trash_id, item.item_type(), item.original_id()); "Found item in trash: ID={}, Type={:?}, OriginalID={}",
trash_id,
item.item_type(),
item.original_id()
);
// Restore based on type // Restore based on type
match item.item_type() { match item.item_type() {
@@ -318,16 +366,26 @@ impl TrashUseCase for TrashService {
// Restore the file to its original location // Restore the file to its original location
let file_id = item.original_id().to_string(); let file_id = item.original_id().to_string();
let original_path = item.original_path().to_string(); let original_path = item.original_path().to_string();
info!("Restoring file from trash: ID={}, OriginalPath={}", file_id, original_path); info!(
match self.file_write_port.restore_from_trash(&file_id, &original_path).await { "Restoring file from trash: ID={}, OriginalPath={}",
file_id, original_path
);
match self
.file_write_port
.restore_from_trash(&file_id, &original_path)
.await
{
Ok(_) => { Ok(_) => {
info!("Successfully restored file from trash: {}", file_id); info!("Successfully restored file from trash: {}", file_id);
}, }
Err(e) => { Err(e) => {
// Check if the error is because the file is not found // Check if the error is because the file is not found
if format!("{}", e).contains("not found") { if format!("{}", e).contains("not found") {
info!("File not found in trash, may already have been restored: {}", file_id); info!(
"File not found in trash, may already have been restored: {}",
file_id
);
// We continue so we can clean up the trash entry // We continue so we can clean up the trash entry
} else { } else {
// Return error for other kinds of errors // Return error for other kinds of errors
@@ -335,68 +393,103 @@ impl TrashUseCase for TrashService {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"File", "File",
format!("Error restoring file {} from trash: {}", file_id, e) format!(
"Error restoring file {} from trash: {}",
file_id, e
),
)); ));
} }
} }
} }
}, }
TrashedItemType::Folder => { TrashedItemType::Folder => {
// Restore the folder to its original location // Restore the folder to its original location
let folder_id = item.original_id().to_string(); let folder_id = item.original_id().to_string();
let original_path = item.original_path().to_string(); let original_path = item.original_path().to_string();
info!("Restoring folder from trash: ID={}, OriginalPath={}", folder_id, original_path); info!(
match self.folder_storage_port.restore_from_trash(&folder_id, &original_path).await { "Restoring folder from trash: ID={}, OriginalPath={}",
folder_id, original_path
);
match self
.folder_storage_port
.restore_from_trash(&folder_id, &original_path)
.await
{
Ok(_) => { Ok(_) => {
info!("Successfully restored folder from trash: {}", folder_id); info!("Successfully restored folder from trash: {}", folder_id);
}, }
Err(e) => { Err(e) => {
// Check if the error is because the folder is not found // Check if the error is because the folder is not found
if format!("{}", e).contains("not found") { if format!("{}", e).contains("not found") {
info!("Folder not found in trash, may already have been restored: {}", folder_id); info!(
"Folder not found in trash, may already have been restored: {}",
folder_id
);
// We continue so we can clean up the trash entry // We continue so we can clean up the trash entry
} else { } else {
// Return error for other kinds of errors // Return error for other kinds of errors
error!("Error restoring folder from trash: {} - {}", folder_id, e); error!(
"Error restoring folder from trash: {} - {}",
folder_id, e
);
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"Folder", "Folder",
format!("Error restoring folder {} from trash: {}", folder_id, e) format!(
"Error restoring folder {} from trash: {}",
folder_id, e
),
)); ));
} }
} }
} }
} }
} }
// Always remove the item from the trash index to maintain consistency // Always remove the item from the trash index to maintain consistency
info!("Removing item from trash index after restoration: {}", trash_id); info!(
match self.trash_repository.restore_from_trash(&trash_uuid, &user_uuid).await { "Removing item from trash index after restoration: {}",
trash_id
);
match self
.trash_repository
.restore_from_trash(&trash_uuid, &user_uuid)
.await
{
Ok(_) => { Ok(_) => {
info!("Successfully removed entry from trash index: {}", trash_id); info!("Successfully removed entry from trash index: {}", trash_id);
}, }
Err(e) => { Err(e) => {
error!("Error removing entry from trash index: {} - {}", trash_id, e); error!(
"Error removing entry from trash index: {} - {}",
trash_id, e
);
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"Trash", "Trash",
format!("Error removing trash entry after restoration: {}", e) format!("Error removing trash entry after restoration: {}", e),
)); ));
} }
} }
info!("Item successfully restored from trash: {}", trash_id); info!("Item successfully restored from trash: {}", trash_id);
Ok(()) Ok(())
}, }
Ok(None) => { Ok(None) => {
// If the item isn't found in trash, we can just return success // If the item isn't found in trash, we can just return success
info!("Item not found in trash index, considering as already restored: {}", trash_id); info!(
"Item not found in trash index, considering as already restored: {}",
trash_id
);
Ok(()) Ok(())
}, }
Err(e) => { Err(e) => {
// Something went wrong with the repository // Something went wrong with the repository
error!("Error retrieving item from trash repository: {} - {}", trash_id, e); error!(
"Error retrieving item from trash repository: {} - {}",
trash_id, e
);
Err(e) Err(e)
} }
} }
@@ -404,121 +497,169 @@ impl TrashUseCase for TrashService {
#[instrument(skip(self))] #[instrument(skip(self))]
async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()> { async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()> {
info!("Permanently deleting item {} for user {}", trash_id, user_id); info!(
"Permanently deleting item {} for user {}",
trash_id, user_id
);
let trash_uuid = match Uuid::parse_str(trash_id) { let trash_uuid = match Uuid::parse_str(trash_id) {
Ok(id) => { Ok(id) => {
info!("Trash UUID parsed successfully: {}", id); info!("Trash UUID parsed successfully: {}", id);
id id
}, }
Err(e) => { Err(e) => {
error!("Invalid trash ID format: {} - {}", trash_id, e); error!("Invalid trash ID format: {} - {}", trash_id, e);
return Err(DomainError::validation_error(format!("Invalid trash ID: {}", e))); return Err(DomainError::validation_error(format!(
"Invalid trash ID: {}",
e
)));
} }
}; };
let user_uuid = match Uuid::parse_str(user_id) { let user_uuid = match Uuid::parse_str(user_id) {
Ok(id) => { Ok(id) => {
info!("User UUID parsed successfully: {}", id); info!("User UUID parsed successfully: {}", id);
id id
}, }
Err(e) => { Err(e) => {
error!("Invalid user ID format: {} - {}", user_id, e); error!("Invalid user ID format: {} - {}", user_id, e);
return Err(DomainError::validation_error(format!("Invalid user ID: {}", e))); return Err(DomainError::validation_error(format!(
"Invalid user ID: {}",
e
)));
} }
}; };
// Get the trash item // Get the trash item
info!("Retrieving trash item from repository: ID={}", trash_id); info!("Retrieving trash item from repository: ID={}", trash_id);
let item_result = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await; let item_result = self
.trash_repository
.get_trash_item(&trash_uuid, &user_uuid)
.await;
match item_result { match item_result {
Ok(Some(item)) => { Ok(Some(item)) => {
info!("Found item in trash: ID={}, Type={:?}, OriginalID={}", info!(
trash_id, item.item_type(), item.original_id()); "Found item in trash: ID={}, Type={:?}, OriginalID={}",
trash_id,
item.item_type(),
item.original_id()
);
// Permanently delete based on type // Permanently delete based on type
match item.item_type() { match item.item_type() {
TrashedItemType::File => { TrashedItemType::File => {
// Permanently delete the file // Permanently delete the file
let file_id = item.original_id().to_string(); let file_id = item.original_id().to_string();
info!("Permanently deleting file: {}", file_id); info!("Permanently deleting file: {}", file_id);
match self.file_write_port.delete_file_permanently(&file_id).await { match self.file_write_port.delete_file_permanently(&file_id).await {
Ok(_) => { Ok(_) => {
info!("Successfully deleted file permanently: {}", file_id); info!("Successfully deleted file permanently: {}", file_id);
}, }
Err(e) => { Err(e) => {
// Check if the file is not found - in that case, we can continue // 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 // because we still want to remove the item from the trash index
if format!("{}", e).contains("not found") { if format!("{}", e).contains("not found") {
info!("File not found, may already have been deleted: {}", file_id); info!(
"File not found, may already have been deleted: {}",
file_id
);
} else { } else {
// Return error for other types of errors // Return error for other types of errors
error!("Error permanently deleting file: {} - {}", file_id, e); error!("Error permanently deleting file: {} - {}", file_id, e);
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"File", "File",
format!("Error deleting file {} permanently: {}", file_id, e) format!(
"Error deleting file {} permanently: {}",
file_id, e
),
)); ));
} }
} }
} }
}, }
TrashedItemType::Folder => { TrashedItemType::Folder => {
// Permanently delete the folder // Permanently delete the folder
let folder_id = item.original_id().to_string(); let folder_id = item.original_id().to_string();
info!("Permanently deleting folder: {}", folder_id); info!("Permanently deleting folder: {}", folder_id);
match self.folder_storage_port.delete_folder_permanently(&folder_id).await { match self
.folder_storage_port
.delete_folder_permanently(&folder_id)
.await
{
Ok(_) => { Ok(_) => {
info!("Successfully deleted folder permanently: {}", folder_id); info!("Successfully deleted folder permanently: {}", folder_id);
}, }
Err(e) => { Err(e) => {
// Check if the folder is not found - in that case, we can continue // Check if the folder is not found - in that case, we can continue
if format!("{}", e).contains("not found") { if format!("{}", e).contains("not found") {
info!("Folder not found, may already have been deleted: {}", folder_id); info!(
"Folder not found, may already have been deleted: {}",
folder_id
);
} else { } else {
// Return error for other types of errors // Return error for other types of errors
error!("Error permanently deleting folder: {} - {}", folder_id, e); error!(
"Error permanently deleting folder: {} - {}",
folder_id, e
);
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"Folder", "Folder",
format!("Error deleting folder {} permanently: {}", folder_id, e) format!(
"Error deleting folder {} permanently: {}",
folder_id, e
),
)); ));
} }
} }
} }
} }
} }
// Always remove the item from trash index to maintain consistency // Always remove the item from trash index to maintain consistency
info!("Removing entry from trash index: {}", trash_id); info!("Removing entry from trash index: {}", trash_id);
match self.trash_repository.delete_permanently(&trash_uuid, &user_uuid).await { match self
.trash_repository
.delete_permanently(&trash_uuid, &user_uuid)
.await
{
Ok(_) => { Ok(_) => {
info!("Successfully removed entry from trash index: {}", trash_id); info!("Successfully removed entry from trash index: {}", trash_id);
}, }
Err(e) => { Err(e) => {
error!("Error removing entry from trash index: {} - {}", trash_id, e); error!(
"Error removing entry from trash index: {} - {}",
trash_id, e
);
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"Trash", "Trash",
format!("Error removing trash entry: {}", e) format!("Error removing trash entry: {}", e),
)); ));
} }
}; };
info!("Item permanently deleted from trash: {}", trash_id); info!("Item permanently deleted from trash: {}", trash_id);
Ok(()) Ok(())
}, }
Ok(None) => { Ok(None) => {
// If the item isn't found in trash, we can just return success // If the item isn't found in trash, we can just return success
info!("Item not found in trash, considering as already deleted: {}", trash_id); info!(
"Item not found in trash, considering as already deleted: {}",
trash_id
);
Ok(()) Ok(())
}, }
Err(e) => { Err(e) => {
// Something went wrong with the repository // Something went wrong with the repository
error!("Error retrieving item from trash repository: {} - {}", trash_id, e); error!(
"Error retrieving item from trash repository: {} - {}",
trash_id, e
);
Err(e) Err(e)
} }
} }
@@ -527,13 +668,13 @@ impl TrashUseCase for TrashService {
#[instrument(skip(self))] #[instrument(skip(self))]
async fn empty_trash(&self, user_id: &str) -> Result<()> { async fn empty_trash(&self, user_id: &str) -> Result<()> {
info!("Emptying trash for user {}", user_id); info!("Emptying trash for user {}", user_id);
let user_uuid = Uuid::parse_str(user_id) let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
// Get all items in the user's trash // Get all items in the user's trash
let items = self.trash_repository.get_trash_items(&user_uuid).await?; let items = self.trash_repository.get_trash_items(&user_uuid).await?;
// Permanently delete each item // Permanently delete each item
for item in items { for item in items {
match item.item_type() { match item.item_type() {
@@ -543,21 +684,25 @@ impl TrashUseCase for TrashService {
if let Err(e) = self.file_write_port.delete_file_permanently(&file_id).await { if let Err(e) = self.file_write_port.delete_file_permanently(&file_id).await {
error!("Error permanently deleting file {}: {}", file_id, e); error!("Error permanently deleting file {}: {}", file_id, e);
} }
}, }
TrashedItemType::Folder => { TrashedItemType::Folder => {
// Permanently delete the folder // Permanently delete the folder
let folder_id = item.original_id().to_string(); let folder_id = item.original_id().to_string();
if let Err(e) = self.folder_storage_port.delete_folder_permanently(&folder_id).await { if let Err(e) = self
.folder_storage_port
.delete_folder_permanently(&folder_id)
.await
{
error!("Error permanently deleting folder {}: {}", folder_id, e); error!("Error permanently deleting folder {}: {}", folder_id, e);
} }
} }
} }
} }
// Clear all trash records for this user // Clear all trash records for this user
self.trash_repository.clear_trash(&user_uuid).await?; self.trash_repository.clear_trash(&user_uuid).await?;
info!("Trash completely emptied for user {}", user_id); info!("Trash completely emptied for user {}", user_id);
Ok(()) Ok(())
} }
} }
+286 -121
View File
@@ -1,21 +1,21 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::path::PathBuf;
use chrono::Utc;
use async_trait::async_trait; use async_trait::async_trait;
use uuid::Uuid;
use bytes::Bytes; use bytes::Bytes;
use chrono::Utc;
use futures::Stream; use futures::Stream;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
use crate::common::errors::{Result, DomainError}; use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::application::services::trash_service::TrashService;
use crate::common::errors::{DomainError, Result};
use crate::domain::entities::file::File; use crate::domain::entities::file::File;
use crate::domain::entities::folder::Folder; use crate::domain::entities::folder::Folder;
use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType}; use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
use crate::domain::repositories::folder_repository::FolderRepository;
use crate::domain::repositories::trash_repository::TrashRepository; use crate::domain::repositories::trash_repository::TrashRepository;
use crate::domain::services::path_service::StoragePath; use crate::domain::services::path_service::StoragePath;
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::domain::repositories::folder_repository::FolderRepository;
use crate::application::services::trash_service::TrashService;
// Mock repositories for testing // Mock repositories for testing
struct MockTrashRepository { struct MockTrashRepository {
@@ -40,7 +40,8 @@ impl TrashRepository for MockTrashRepository {
async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> { async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> {
let items = self.trash_items.lock().unwrap(); let items = self.trash_items.lock().unwrap();
let user_items = items.values() let user_items = items
.values()
.filter(|item| item.user_id() == *user_id) .filter(|item| item.user_id() == *user_id)
.cloned() .cloned()
.collect(); .collect();
@@ -49,7 +50,8 @@ impl TrashRepository for MockTrashRepository {
async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result<Option<TrashedItem>> { async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result<Option<TrashedItem>> {
let items = self.trash_items.lock().unwrap(); let items = self.trash_items.lock().unwrap();
let item = items.get(id) let item = items
.get(id)
.filter(|item| item.user_id() == *user_id) .filter(|item| item.user_id() == *user_id)
.cloned(); .cloned();
Ok(item) Ok(item)
@@ -58,18 +60,20 @@ impl TrashRepository for MockTrashRepository {
async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> { async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
let mut items = self.trash_items.lock().unwrap(); let mut items = self.trash_items.lock().unwrap();
if let Some(item) = items.get(id) if let Some(item) = items.get(id)
&& item.user_id() == *user_id { && item.user_id() == *user_id
items.remove(id); {
} items.remove(id);
}
Ok(()) Ok(())
} }
async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> { async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
let mut items = self.trash_items.lock().unwrap(); let mut items = self.trash_items.lock().unwrap();
if let Some(item) = items.get(id) if let Some(item) = items.get(id)
&& item.user_id() == *user_id { && item.user_id() == *user_id
items.remove(id); {
} items.remove(id);
}
Ok(()) Ok(())
} }
@@ -82,7 +86,8 @@ impl TrashRepository for MockTrashRepository {
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> { async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> {
let items = self.trash_items.lock().unwrap(); let items = self.trash_items.lock().unwrap();
let now = Utc::now(); let now = Utc::now();
let expired = items.values() let expired = items
.values()
.filter(|item| item.deletion_date() <= now) .filter(|item| item.deletion_date() <= now)
.cloned() .cloned()
.collect(); .collect();
@@ -111,8 +116,9 @@ impl MockFileRepository {
100, 100,
"text/plain".to_string(), "text/plain".to_string(),
None, None,
).unwrap(); )
.unwrap();
let mut files = self.files.lock().unwrap(); let mut files = self.files.lock().unwrap();
files.insert(id.to_string(), file); files.insert(id.to_string(), file);
} }
@@ -129,7 +135,10 @@ impl FileReadPort for MockFileRepository {
} }
} }
async fn list_files(&self, _folder_id: Option<&str>) -> std::result::Result<Vec<File>, DomainError> { async fn list_files(
&self,
_folder_id: Option<&str>,
) -> std::result::Result<Vec<File>, DomainError> {
Ok(vec![]) Ok(vec![])
} }
@@ -140,7 +149,10 @@ impl FileReadPort for MockFileRepository {
async fn get_file_stream( async fn get_file_stream(
&self, &self,
_id: &str, _id: &str,
) -> std::result::Result<Box<dyn Stream<Item = std::result::Result<Bytes, std::io::Error>> + Send>, DomainError> { ) -> std::result::Result<
Box<dyn Stream<Item = std::result::Result<Bytes, std::io::Error>> + Send>,
DomainError,
> {
unimplemented!() unimplemented!()
} }
@@ -149,7 +161,10 @@ impl FileReadPort for MockFileRepository {
_id: &str, _id: &str,
_start: u64, _start: u64,
_end: Option<u64>, _end: Option<u64>,
) -> std::result::Result<Box<dyn Stream<Item = std::result::Result<Bytes, std::io::Error>> + Send>, DomainError> { ) -> std::result::Result<
Box<dyn Stream<Item = std::result::Result<Bytes, std::io::Error>> + Send>,
DomainError,
> {
unimplemented!() unimplemented!()
} }
@@ -183,7 +198,9 @@ impl FileWritePort for MockFileRepository {
_name: String, _name: String,
_folder_id: Option<String>, _folder_id: Option<String>,
_content_type: String, _content_type: String,
_stream: std::pin::Pin<Box<dyn Stream<Item = std::result::Result<Bytes, std::io::Error>> + Send>>, _stream: std::pin::Pin<
Box<dyn Stream<Item = std::result::Result<Bytes, std::io::Error>> + Send>,
>,
) -> std::result::Result<File, DomainError> { ) -> std::result::Result<File, DomainError> {
unimplemented!() unimplemented!()
} }
@@ -208,7 +225,11 @@ impl FileWritePort for MockFileRepository {
Ok(()) Ok(())
} }
async fn update_file_content(&self, _file_id: &str, _content: Vec<u8>) -> std::result::Result<(), DomainError> { async fn update_file_content(
&self,
_file_id: &str,
_content: Vec<u8>,
) -> std::result::Result<(), DomainError> {
Ok(()) Ok(())
} }
@@ -225,7 +246,7 @@ impl FileWritePort for MockFileRepository {
async fn move_to_trash(&self, id: &str) -> std::result::Result<(), DomainError> { async fn move_to_trash(&self, id: &str) -> std::result::Result<(), DomainError> {
let mut files = self.files.lock().unwrap(); let mut files = self.files.lock().unwrap();
let mut trashed = self.trashed_files.lock().unwrap(); let mut trashed = self.trashed_files.lock().unwrap();
if let Some(file) = files.remove(id) { if let Some(file) = files.remove(id) {
trashed.insert(id.to_string(), file); trashed.insert(id.to_string(), file);
Ok(()) Ok(())
@@ -234,15 +255,22 @@ impl FileWritePort for MockFileRepository {
} }
} }
async fn restore_from_trash(&self, id: &str, _original_path: &str) -> std::result::Result<(), DomainError> { async fn restore_from_trash(
&self,
id: &str,
_original_path: &str,
) -> std::result::Result<(), DomainError> {
let mut files = self.files.lock().unwrap(); let mut files = self.files.lock().unwrap();
let mut trashed = self.trashed_files.lock().unwrap(); let mut trashed = self.trashed_files.lock().unwrap();
if let Some(file) = trashed.remove(id) { if let Some(file) = trashed.remove(id) {
files.insert(id.to_string(), file); files.insert(id.to_string(), file);
Ok(()) Ok(())
} else { } else {
Err(DomainError::not_found("File", format!("File {} not found in trash", id))) Err(DomainError::not_found(
"File",
format!("File {} not found in trash", id),
))
} }
} }
@@ -251,7 +279,10 @@ impl FileWritePort for MockFileRepository {
if trashed.remove(id).is_some() { if trashed.remove(id).is_some() {
Ok(()) Ok(())
} else { } else {
Err(DomainError::not_found("File", format!("File {} not found in trash", id))) Err(DomainError::not_found(
"File",
format!("File {} not found in trash", id),
))
} }
} }
} }
@@ -275,8 +306,9 @@ impl MockFolderRepository {
name.to_string(), name.to_string(),
StoragePath::from_string(path), StoragePath::from_string(path),
None, None,
).unwrap(); )
.unwrap();
let mut folders = self.folders.lock().unwrap(); let mut folders = self.folders.lock().unwrap();
folders.insert(id.to_string(), folder); folders.insert(id.to_string(), folder);
} }
@@ -284,7 +316,11 @@ impl MockFolderRepository {
#[async_trait] #[async_trait]
impl FolderRepository for MockFolderRepository { impl FolderRepository for MockFolderRepository {
async fn create_folder(&self, _name: String, _parent_id: Option<String>) -> std::result::Result<Folder, DomainError> { async fn create_folder(
&self,
_name: String,
_parent_id: Option<String>,
) -> std::result::Result<Folder, DomainError> {
unimplemented!() unimplemented!()
} }
@@ -297,11 +333,17 @@ impl FolderRepository for MockFolderRepository {
} }
} }
async fn get_folder_by_path(&self, _storage_path: &StoragePath) -> std::result::Result<Folder, DomainError> { async fn get_folder_by_path(
&self,
_storage_path: &StoragePath,
) -> std::result::Result<Folder, DomainError> {
unimplemented!() unimplemented!()
} }
async fn list_folders(&self, _parent_id: Option<&str>) -> std::result::Result<Vec<Folder>, DomainError> { async fn list_folders(
&self,
_parent_id: Option<&str>,
) -> std::result::Result<Vec<Folder>, DomainError> {
Ok(vec![]) Ok(vec![])
} }
@@ -315,11 +357,19 @@ impl FolderRepository for MockFolderRepository {
Ok((vec![], Some(0))) Ok((vec![], Some(0)))
} }
async fn rename_folder(&self, _id: &str, _new_name: String) -> std::result::Result<Folder, DomainError> { async fn rename_folder(
&self,
_id: &str,
_new_name: String,
) -> std::result::Result<Folder, DomainError> {
unimplemented!() unimplemented!()
} }
async fn move_folder(&self, _id: &str, _new_parent_id: Option<&str>) -> std::result::Result<Folder, DomainError> { async fn move_folder(
&self,
_id: &str,
_new_parent_id: Option<&str>,
) -> std::result::Result<Folder, DomainError> {
unimplemented!() unimplemented!()
} }
@@ -327,7 +377,10 @@ impl FolderRepository for MockFolderRepository {
Ok(()) Ok(())
} }
async fn folder_exists(&self, _storage_path: &StoragePath) -> std::result::Result<bool, DomainError> { async fn folder_exists(
&self,
_storage_path: &StoragePath,
) -> std::result::Result<bool, DomainError> {
Ok(false) Ok(false)
} }
@@ -338,7 +391,7 @@ impl FolderRepository for MockFolderRepository {
async fn move_to_trash(&self, id: &str) -> std::result::Result<(), DomainError> { async fn move_to_trash(&self, id: &str) -> std::result::Result<(), DomainError> {
let mut folders = self.folders.lock().unwrap(); let mut folders = self.folders.lock().unwrap();
let mut trashed = self.trashed_folders.lock().unwrap(); let mut trashed = self.trashed_folders.lock().unwrap();
if let Some(folder) = folders.remove(id) { if let Some(folder) = folders.remove(id) {
trashed.insert(id.to_string(), folder); trashed.insert(id.to_string(), folder);
Ok(()) Ok(())
@@ -347,15 +400,22 @@ impl FolderRepository for MockFolderRepository {
} }
} }
async fn restore_from_trash(&self, id: &str, _original_path: &str) -> std::result::Result<(), DomainError> { async fn restore_from_trash(
&self,
id: &str,
_original_path: &str,
) -> std::result::Result<(), DomainError> {
let mut folders = self.folders.lock().unwrap(); let mut folders = self.folders.lock().unwrap();
let mut trashed = self.trashed_folders.lock().unwrap(); let mut trashed = self.trashed_folders.lock().unwrap();
if let Some(folder) = trashed.remove(id) { if let Some(folder) = trashed.remove(id) {
folders.insert(id.to_string(), folder); folders.insert(id.to_string(), folder);
Ok(()) Ok(())
} else { } else {
Err(DomainError::not_found("Folder", format!("Folder {} not found in trash", id))) Err(DomainError::not_found(
"Folder",
format!("Folder {} not found in trash", id),
))
} }
} }
@@ -364,7 +424,10 @@ impl FolderRepository for MockFolderRepository {
if trashed.remove(id).is_some() { if trashed.remove(id).is_some() {
Ok(()) Ok(())
} else { } else {
Err(DomainError::not_found("Folder", format!("Folder {} not found in trash", id))) Err(DomainError::not_found(
"Folder",
format!("Folder {} not found in trash", id),
))
} }
} }
} }
@@ -380,7 +443,7 @@ mod tests {
let trash_repo = Arc::new(MockTrashRepository::new()); let trash_repo = Arc::new(MockTrashRepository::new());
let file_repo = Arc::new(MockFileRepository::new()); let file_repo = Arc::new(MockFileRepository::new());
let folder_repo = Arc::new(MockFolderRepository::new()); let folder_repo = Arc::new(MockFolderRepository::new());
let service = TrashService::new( let service = TrashService::new(
trash_repo.clone(), trash_repo.clone(),
file_repo.clone() as Arc<dyn FileReadPort>, file_repo.clone() as Arc<dyn FileReadPort>,
@@ -388,37 +451,59 @@ mod tests {
folder_repo.clone(), folder_repo.clone(),
30, // 30 days retention 30, // 30 days retention
); );
let file_id = "550e8400-e29b-41d4-a716-446655440000"; let file_id = "550e8400-e29b-41d4-a716-446655440000";
let user_id = "550e8400-e29b-41d4-a716-446655440001"; let user_id = "550e8400-e29b-41d4-a716-446655440001";
// Add a test file to the repository // Add a test file to the repository
file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt"); file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt");
// Act // Act
let result = service.move_to_trash(file_id, "file", user_id).await; let result = service.move_to_trash(file_id, "file", user_id).await;
// Assert // Assert
assert!(result.is_ok(), "Moving file to trash failed: {:?}", result); assert!(result.is_ok(), "Moving file to trash failed: {:?}", result);
// Verify the file is in trash // Verify the file is in trash
let user_uuid = Uuid::parse_str(user_id).unwrap(); let user_uuid = Uuid::parse_str(user_id).unwrap();
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
assert_eq!(trash_items.len(), 1, "Should have exactly one item in trash"); assert_eq!(
trash_items.len(),
1,
"Should have exactly one item in trash"
);
let trash_item = &trash_items[0]; let trash_item = &trash_items[0];
assert_eq!(trash_item.original_id().to_string(), file_id, "Original ID should match file ID"); assert_eq!(
assert_eq!(trash_item.user_id().to_string(), user_id, "User ID should match"); trash_item.original_id().to_string(),
assert_eq!(*trash_item.item_type(), TrashedItemType::File, "Item type should be File"); file_id,
"Original ID should match file ID"
);
assert_eq!(
trash_item.user_id().to_string(),
user_id,
"User ID should match"
);
assert_eq!(
*trash_item.item_type(),
TrashedItemType::File,
"Item type should be File"
);
assert_eq!(trash_item.name(), "test.txt", "File name should match"); assert_eq!(trash_item.name(), "test.txt", "File name should match");
// Verify file is moved in file repository // Verify file is moved in file repository
let files = file_repo.files.lock().unwrap(); let files = file_repo.files.lock().unwrap();
let trashed_files = file_repo.trashed_files.lock().unwrap(); let trashed_files = file_repo.trashed_files.lock().unwrap();
assert!(files.get(file_id).is_none(), "File should no longer be in main storage"); assert!(
assert!(trashed_files.get(file_id).is_some(), "File should be in trash storage"); files.get(file_id).is_none(),
"File should no longer be in main storage"
);
assert!(
trashed_files.get(file_id).is_some(),
"File should be in trash storage"
);
} }
#[tokio::test] #[tokio::test]
@@ -427,7 +512,7 @@ mod tests {
let trash_repo = Arc::new(MockTrashRepository::new()); let trash_repo = Arc::new(MockTrashRepository::new());
let file_repo = Arc::new(MockFileRepository::new()); let file_repo = Arc::new(MockFileRepository::new());
let folder_repo = Arc::new(MockFolderRepository::new()); let folder_repo = Arc::new(MockFolderRepository::new());
let service = TrashService::new( let service = TrashService::new(
trash_repo.clone(), trash_repo.clone(),
file_repo.clone() as Arc<dyn FileReadPort>, file_repo.clone() as Arc<dyn FileReadPort>,
@@ -435,29 +520,49 @@ mod tests {
folder_repo.clone(), folder_repo.clone(),
30, // 30 days retention 30, // 30 days retention
); );
let folder_id = "550e8400-e29b-41d4-a716-446655440002"; let folder_id = "550e8400-e29b-41d4-a716-446655440002";
let user_id = "550e8400-e29b-41d4-a716-446655440001"; let user_id = "550e8400-e29b-41d4-a716-446655440001";
// Add a test folder to the repository // Add a test folder to the repository
folder_repo.add_test_folder(folder_id, "test_folder", "/test/path/test_folder"); folder_repo.add_test_folder(folder_id, "test_folder", "/test/path/test_folder");
// Act // Act
let result = service.move_to_trash(folder_id, "folder", user_id).await; let result = service.move_to_trash(folder_id, "folder", user_id).await;
// Assert // Assert
assert!(result.is_ok(), "Moving folder to trash failed: {:?}", result); assert!(
result.is_ok(),
"Moving folder to trash failed: {:?}",
result
);
// Verify the folder is in trash // Verify the folder is in trash
let user_uuid = Uuid::parse_str(user_id).unwrap(); let user_uuid = Uuid::parse_str(user_id).unwrap();
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
assert_eq!(trash_items.len(), 1, "Should have exactly one item in trash"); assert_eq!(
trash_items.len(),
1,
"Should have exactly one item in trash"
);
let trash_item = &trash_items[0]; let trash_item = &trash_items[0];
assert_eq!(trash_item.original_id().to_string(), folder_id, "Original ID should match folder ID"); assert_eq!(
assert_eq!(trash_item.user_id().to_string(), user_id, "User ID should match"); trash_item.original_id().to_string(),
assert_eq!(*trash_item.item_type(), TrashedItemType::Folder, "Item type should be Folder"); folder_id,
"Original ID should match folder ID"
);
assert_eq!(
trash_item.user_id().to_string(),
user_id,
"User ID should match"
);
assert_eq!(
*trash_item.item_type(),
TrashedItemType::Folder,
"Item type should be Folder"
);
assert_eq!(trash_item.name(), "test_folder", "Folder name should match"); assert_eq!(trash_item.name(), "test_folder", "Folder name should match");
} }
@@ -467,7 +572,7 @@ mod tests {
let trash_repo = Arc::new(MockTrashRepository::new()); let trash_repo = Arc::new(MockTrashRepository::new());
let file_repo = Arc::new(MockFileRepository::new()); let file_repo = Arc::new(MockFileRepository::new());
let folder_repo = Arc::new(MockFolderRepository::new()); let folder_repo = Arc::new(MockFolderRepository::new());
let service = TrashService::new( let service = TrashService::new(
trash_repo.clone(), trash_repo.clone(),
file_repo.clone() as Arc<dyn FileReadPort>, file_repo.clone() as Arc<dyn FileReadPort>,
@@ -475,36 +580,53 @@ mod tests {
folder_repo.clone(), folder_repo.clone(),
30, // 30 days retention 30, // 30 days retention
); );
let file_id = "550e8400-e29b-41d4-a716-446655440000"; let file_id = "550e8400-e29b-41d4-a716-446655440000";
let user_id = "550e8400-e29b-41d4-a716-446655440001"; let user_id = "550e8400-e29b-41d4-a716-446655440001";
let file_path = "/test/path/test.txt"; let file_path = "/test/path/test.txt";
// Add a test file and move it to trash // Add a test file and move it to trash
file_repo.add_test_file(file_id, "test.txt", file_path); file_repo.add_test_file(file_id, "test.txt", file_path);
service.move_to_trash(file_id, "file", user_id).await.unwrap(); service
.move_to_trash(file_id, "file", user_id)
.await
.unwrap();
// Get the trash item ID // Get the trash item ID
let user_uuid = Uuid::parse_str(user_id).unwrap(); let user_uuid = Uuid::parse_str(user_id).unwrap();
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
let trash_id = trash_items[0].id().to_string(); let trash_id = trash_items[0].id().to_string();
// Act // Act
let result = service.restore_item(&trash_id, user_id).await; let result = service.restore_item(&trash_id, user_id).await;
// Assert // Assert
assert!(result.is_ok(), "Restoring file from trash failed: {:?}", result); assert!(
result.is_ok(),
"Restoring file from trash failed: {:?}",
result
);
// Verify the file is restored in file repository // Verify the file is restored in file repository
let files = file_repo.files.lock().unwrap(); let files = file_repo.files.lock().unwrap();
let trashed_files = file_repo.trashed_files.lock().unwrap(); let trashed_files = file_repo.trashed_files.lock().unwrap();
assert!(files.get(file_id).is_some(), "File should be back in main storage"); assert!(
assert!(trashed_files.get(file_id).is_none(), "File should no longer be in trash storage"); files.get(file_id).is_some(),
"File should be back in main storage"
);
assert!(
trashed_files.get(file_id).is_none(),
"File should no longer be in trash storage"
);
// Verify the trash item is removed // Verify the trash item is removed
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
assert_eq!(trash_items.len(), 0, "Trash should be empty after restoration"); assert_eq!(
trash_items.len(),
0,
"Trash should be empty after restoration"
);
} }
#[tokio::test] #[tokio::test]
@@ -513,7 +635,7 @@ mod tests {
let trash_repo = Arc::new(MockTrashRepository::new()); let trash_repo = Arc::new(MockTrashRepository::new());
let file_repo = Arc::new(MockFileRepository::new()); let file_repo = Arc::new(MockFileRepository::new());
let folder_repo = Arc::new(MockFolderRepository::new()); let folder_repo = Arc::new(MockFolderRepository::new());
let service = TrashService::new( let service = TrashService::new(
trash_repo.clone(), trash_repo.clone(),
file_repo.clone() as Arc<dyn FileReadPort>, file_repo.clone() as Arc<dyn FileReadPort>,
@@ -521,35 +643,52 @@ mod tests {
folder_repo.clone(), folder_repo.clone(),
30, // 30 days retention 30, // 30 days retention
); );
let file_id = "550e8400-e29b-41d4-a716-446655440000"; let file_id = "550e8400-e29b-41d4-a716-446655440000";
let user_id = "550e8400-e29b-41d4-a716-446655440001"; let user_id = "550e8400-e29b-41d4-a716-446655440001";
// Add a test file and move it to trash // Add a test file and move it to trash
file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt"); file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt");
service.move_to_trash(file_id, "file", user_id).await.unwrap(); service
.move_to_trash(file_id, "file", user_id)
.await
.unwrap();
// Get the trash item ID // Get the trash item ID
let user_uuid = Uuid::parse_str(user_id).unwrap(); let user_uuid = Uuid::parse_str(user_id).unwrap();
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
let trash_id = trash_items[0].id().to_string(); let trash_id = trash_items[0].id().to_string();
// Act // Act
let result = service.delete_permanently(&trash_id, user_id).await; let result = service.delete_permanently(&trash_id, user_id).await;
// Assert // Assert
assert!(result.is_ok(), "Deleting file permanently failed: {:?}", result); assert!(
result.is_ok(),
"Deleting file permanently failed: {:?}",
result
);
// Verify the file is permanently deleted // Verify the file is permanently deleted
let files = file_repo.files.lock().unwrap(); let files = file_repo.files.lock().unwrap();
let trashed_files = file_repo.trashed_files.lock().unwrap(); let trashed_files = file_repo.trashed_files.lock().unwrap();
assert!(files.get(file_id).is_none(), "File should not be in main storage"); assert!(
assert!(trashed_files.get(file_id).is_none(), "File should not be in trash storage"); files.get(file_id).is_none(),
"File should not be in main storage"
);
assert!(
trashed_files.get(file_id).is_none(),
"File should not be in trash storage"
);
// Verify the trash item is removed // Verify the trash item is removed
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
assert_eq!(trash_items.len(), 0, "Trash should be empty after permanent deletion"); assert_eq!(
trash_items.len(),
0,
"Trash should be empty after permanent deletion"
);
} }
#[tokio::test] #[tokio::test]
@@ -558,7 +697,7 @@ mod tests {
let trash_repo = Arc::new(MockTrashRepository::new()); let trash_repo = Arc::new(MockTrashRepository::new());
let file_repo = Arc::new(MockFileRepository::new()); let file_repo = Arc::new(MockFileRepository::new());
let folder_repo = Arc::new(MockFolderRepository::new()); let folder_repo = Arc::new(MockFolderRepository::new());
let service = TrashService::new( let service = TrashService::new(
trash_repo.clone(), trash_repo.clone(),
file_repo.clone() as Arc<dyn FileReadPort>, file_repo.clone() as Arc<dyn FileReadPort>,
@@ -566,59 +705,85 @@ mod tests {
folder_repo.clone(), folder_repo.clone(),
30, // 30 days retention 30, // 30 days retention
); );
let user_id = "550e8400-e29b-41d4-a716-446655440001"; let user_id = "550e8400-e29b-41d4-a716-446655440001";
// Add multiple files and folders to trash // Add multiple files and folders to trash
let file_ids = [ let file_ids = [
"550e8400-e29b-41d4-a716-446655440010", "550e8400-e29b-41d4-a716-446655440010",
"550e8400-e29b-41d4-a716-446655440011", "550e8400-e29b-41d4-a716-446655440011",
]; ];
let folder_ids = [ let folder_ids = [
"550e8400-e29b-41d4-a716-446655440020", "550e8400-e29b-41d4-a716-446655440020",
"550e8400-e29b-41d4-a716-446655440021", "550e8400-e29b-41d4-a716-446655440021",
]; ];
// Add test files and folders // Add test files and folders
for (i, file_id) in file_ids.iter().enumerate() { for (i, file_id) in file_ids.iter().enumerate() {
file_repo.add_test_file(file_id, &format!("test{}.txt", i), &format!("/test/path/test{}.txt", i)); file_repo.add_test_file(
service.move_to_trash(file_id, "file", user_id).await.unwrap(); file_id,
&format!("test{}.txt", i),
&format!("/test/path/test{}.txt", i),
);
service
.move_to_trash(file_id, "file", user_id)
.await
.unwrap();
} }
for (i, folder_id) in folder_ids.iter().enumerate() { for (i, folder_id) in folder_ids.iter().enumerate() {
folder_repo.add_test_folder(folder_id, &format!("folder{}", i), &format!("/test/path/folder{}", i)); folder_repo.add_test_folder(
service.move_to_trash(folder_id, "folder", user_id).await.unwrap(); folder_id,
&format!("folder{}", i),
&format!("/test/path/folder{}", i),
);
service
.move_to_trash(folder_id, "folder", user_id)
.await
.unwrap();
} }
// Verify items are in trash // Verify items are in trash
let user_uuid = Uuid::parse_str(user_id).unwrap(); let user_uuid = Uuid::parse_str(user_id).unwrap();
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
assert_eq!(trash_items.len(), 4, "Should have 4 items in trash"); assert_eq!(trash_items.len(), 4, "Should have 4 items in trash");
// Act // Act
let result = service.empty_trash(user_id).await; let result = service.empty_trash(user_id).await;
// Assert // Assert
assert!(result.is_ok(), "Emptying trash failed: {:?}", result); assert!(result.is_ok(), "Emptying trash failed: {:?}", result);
// Verify all items are permanently deleted // Verify all items are permanently deleted
for file_id in &file_ids { for file_id in &file_ids {
let files = file_repo.files.lock().unwrap(); let files = file_repo.files.lock().unwrap();
let trashed_files = file_repo.trashed_files.lock().unwrap(); let trashed_files = file_repo.trashed_files.lock().unwrap();
assert!(files.get(*file_id).is_none(), "File should not be in main storage"); assert!(
assert!(trashed_files.get(*file_id).is_none(), "File should not be in trash storage"); files.get(*file_id).is_none(),
"File should not be in main storage"
);
assert!(
trashed_files.get(*file_id).is_none(),
"File should not be in trash storage"
);
} }
for folder_id in &folder_ids { for folder_id in &folder_ids {
let folders = folder_repo.folders.lock().unwrap(); let folders = folder_repo.folders.lock().unwrap();
let trashed_folders = folder_repo.trashed_folders.lock().unwrap(); let trashed_folders = folder_repo.trashed_folders.lock().unwrap();
assert!(folders.get(*folder_id).is_none(), "Folder should not be in main storage"); assert!(
assert!(trashed_folders.get(*folder_id).is_none(), "Folder should not be in trash storage"); folders.get(*folder_id).is_none(),
"Folder should not be in main storage"
);
assert!(
trashed_folders.get(*folder_id).is_none(),
"Folder should not be in trash storage"
);
} }
// Verify the trash is empty // Verify the trash is empty
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
assert_eq!(trash_items.len(), 0, "Trash should be empty after emptying"); assert_eq!(trash_items.len(), 0, "Trash should be empty after emptying");
} }
} }
+1 -1
View File
@@ -1 +1 @@
pub mod storage_transaction; pub mod storage_transaction;
@@ -1,6 +1,6 @@
use crate::common::errors::{DomainError, ErrorKind};
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use crate::common::errors::{DomainError, ErrorKind};
/// Type for async operations and rollbacks /// Type for async operations and rollbacks
type TransactionOp = Pin<Box<dyn Future<Output = Result<(), DomainError>> + Send>>; type TransactionOp = Pin<Box<dyn Future<Output = Result<(), DomainError>> + Send>>;
@@ -25,7 +25,7 @@ impl StorageTransaction {
name: name.to_string(), name: name.to_string(),
} }
} }
/// Adds an operation to the transaction with its corresponding rollback /// Adds an operation to the transaction with its corresponding rollback
pub fn add_operation<F, R>(&mut self, operation: F, rollback: R) pub fn add_operation<F, R>(&mut self, operation: F, rollback: R)
where where
@@ -35,7 +35,7 @@ impl StorageTransaction {
self.operations.push(Box::new(move || Box::pin(operation))); self.operations.push(Box::new(move || Box::pin(operation)));
self.rollbacks.push(Box::new(move || Box::pin(rollback))); self.rollbacks.push(Box::new(move || Box::pin(rollback)));
} }
/// Adds an operation without rollback (for cleanup or logging) /// Adds an operation without rollback (for cleanup or logging)
pub fn add_finalizer<F>(&mut self, finalizer: F) pub fn add_finalizer<F>(&mut self, finalizer: F)
where where
@@ -43,58 +43,68 @@ impl StorageTransaction {
{ {
// The rollback is a no-op // The rollback is a no-op
let noop = async { Ok(()) }; let noop = async { Ok(()) };
self.operations.push(Box::new(move || Box::pin(finalizer))); self.operations.push(Box::new(move || Box::pin(finalizer)));
self.rollbacks.push(Box::new(move || Box::pin(noop))); self.rollbacks.push(Box::new(move || Box::pin(noop)));
} }
/// Executes the transaction by applying all operations in order /// Executes the transaction by applying all operations in order
/// If any fails, executes rollbacks in reverse order /// If any fails, executes rollbacks in reverse order
pub async fn commit(mut self) -> Result<(), DomainError> { pub async fn commit(mut self) -> Result<(), DomainError> {
tracing::debug!("Starting transaction: {}", self.name); tracing::debug!("Starting transaction: {}", self.name);
let mut completed_ops = Vec::new(); let mut completed_ops = Vec::new();
// Extract operations to avoid ownership issues // Extract operations to avoid ownership issues
let operations = std::mem::take(&mut self.operations); let operations = std::mem::take(&mut self.operations);
let transaction_name = self.name.clone(); let transaction_name = self.name.clone();
// Execute operations // Execute operations
for (i, op) in operations.into_iter().enumerate() { for (i, op) in operations.into_iter().enumerate() {
match op().await { match op().await {
Ok(()) => { Ok(()) => {
completed_ops.push(i); completed_ops.push(i);
tracing::trace!("Operation {} completed in transaction: {}", i, transaction_name); tracing::trace!(
"Operation {} completed in transaction: {}",
i,
transaction_name
);
} }
Err(e) => { Err(e) => {
tracing::error!("Error in operation {} of transaction {}: {}", i, transaction_name, e); tracing::error!(
"Error in operation {} of transaction {}: {}",
i,
transaction_name,
e
);
// Execute rollbacks for completed operations in reverse order // Execute rollbacks for completed operations in reverse order
self.rollback(completed_ops).await?; self.rollback(completed_ops).await?;
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"Transaction", "Transaction",
format!("Transaction '{}' failed: {}", transaction_name, e) format!("Transaction '{}' failed: {}", transaction_name, e),
).with_source(e)); )
.with_source(e));
} }
} }
} }
tracing::debug!("Transaction completed successfully: {}", transaction_name); tracing::debug!("Transaction completed successfully: {}", transaction_name);
Ok(()) Ok(())
} }
/// Executes rollbacks for completed operations /// Executes rollbacks for completed operations
async fn rollback(mut self, completed_ops: Vec<usize>) -> Result<(), DomainError> { async fn rollback(mut self, completed_ops: Vec<usize>) -> Result<(), DomainError> {
tracing::warn!("Starting rollback for transaction: {}", self.name); tracing::warn!("Starting rollback for transaction: {}", self.name);
let mut rollback_errors = Vec::new(); let mut rollback_errors = Vec::new();
// Extract rollbacks to avoid ownership issues // Extract rollbacks to avoid ownership issues
let mut rollbacks = Vec::new(); let mut rollbacks = Vec::new();
std::mem::swap(&mut rollbacks, &mut self.rollbacks); std::mem::swap(&mut rollbacks, &mut self.rollbacks);
// Execute rollbacks in reverse order // Execute rollbacks in reverse order
for i in completed_ops.into_iter().rev() { for i in completed_ops.into_iter().rev() {
if i < rollbacks.len() { if i < rollbacks.len() {
@@ -103,28 +113,38 @@ impl StorageTransaction {
// Swap with an empty function // Swap with an empty function
let rollback = std::mem::replace(rb, Box::new(|| Box::pin(async { Ok(()) }))); let rollback = std::mem::replace(rb, Box::new(|| Box::pin(async { Ok(()) })));
if let Err(e) = rollback().await { if let Err(e) = rollback().await {
tracing::error!("Error in rollback of operation {} in transaction {}: {}", tracing::error!(
i, self.name, e); "Error in rollback of operation {} in transaction {}: {}",
i,
self.name,
e
);
rollback_errors.push(e); rollback_errors.push(e);
} }
} }
} }
} }
// If there were errors during rollback, report them // If there were errors during rollback, report them
if !rollback_errors.is_empty() { if !rollback_errors.is_empty() {
tracing::error!("Errors during transaction rollback {}: {} errors", tracing::error!(
self.name, rollback_errors.len()); "Errors during transaction rollback {}: {} errors",
self.name,
rollback_errors.len()
);
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"Transaction", "Transaction",
format!("Errors during transaction '{}' rollback: {} errors", format!(
self.name, rollback_errors.len()) "Errors during transaction '{}' rollback: {} errors",
self.name,
rollback_errors.len()
),
)); ));
} }
tracing::info!("Transaction rollback completed: {}", self.name); tracing::info!("Transaction rollback completed: {}", self.name);
Ok(()) Ok(())
} }
} }
+11 -11
View File
@@ -7,7 +7,7 @@ use std::time::Duration;
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure logging // Configure logging
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
// Load environment variables (.env.local first, then .env) // Load environment variables (.env.local first, then .env)
if let Ok(path) = env::var("DOTENV_PATH") { if let Ok(path) = env::var("DOTENV_PATH") {
dotenv::from_path(Path::new(&path)).ok(); dotenv::from_path(Path::new(&path)).ok();
@@ -15,35 +15,35 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenv::from_filename(".env.local").ok(); dotenv::from_filename(".env.local").ok();
dotenv::dotenv().ok(); dotenv::dotenv().ok();
} }
// Get DATABASE_URL from environment variables // Get DATABASE_URL from environment variables
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be configured"); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be configured");
println!("Connecting to the database..."); println!("Connecting to the database...");
// Create connection pool // Create connection pool
let pool = PgPoolOptions::new() let pool = PgPoolOptions::new()
.max_connections(5) .max_connections(5)
.acquire_timeout(Duration::from_secs(10)) .acquire_timeout(Duration::from_secs(10))
.connect(&database_url) .connect(&database_url)
.await?; .await?;
// Run migrations // Run migrations
println!("Running migrations..."); println!("Running migrations...");
// Get the directory from an environment variable or use a default value // Get the directory from an environment variable or use a default value
let migrations_dir = env::var("MIGRATIONS_DIR").unwrap_or_else(|_| "./migrations".to_string()); let migrations_dir = env::var("MIGRATIONS_DIR").unwrap_or_else(|_| "./migrations".to_string());
println!("Migrations directory: {}", migrations_dir); println!("Migrations directory: {}", migrations_dir);
// Create a migrator // Create a migrator
let migrator = sqlx::migrate::Migrator::new(Path::new(&migrations_dir)) let migrator = sqlx::migrate::Migrator::new(Path::new(&migrations_dir))
.await .await
.expect("Could not create the migrator"); .expect("Could not create the migrator");
// Run all pending migrations // Run all pending migrations
migrator.run(&pool).await?; migrator.run(&pool).await?;
println!("Migrations applied successfully"); println!("Migrations applied successfully");
Ok(()) Ok(())
} }
+133 -105
View File
@@ -1,6 +1,6 @@
use std::time::Duration;
use std::path::PathBuf;
use std::env; use std::env;
use std::path::PathBuf;
use std::time::Duration;
/// Cache configuration /// Cache configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -16,9 +16,9 @@ pub struct CacheConfig {
impl Default for CacheConfig { impl Default for CacheConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
file_ttl_ms: 60_000, // 1 minute file_ttl_ms: 60_000, // 1 minute
directory_ttl_ms: 120_000, // 2 minutes directory_ttl_ms: 120_000, // 2 minutes
max_entries: 10_000, // 10,000 entries max_entries: 10_000, // 10,000 entries
} }
} }
} }
@@ -100,10 +100,10 @@ pub struct ResourceConfig {
impl Default for ResourceConfig { impl Default for ResourceConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
large_file_threshold_mb: 100, // 100 MB large_file_threshold_mb: 100, // 100 MB
large_dir_threshold_entries: 1000, // 1000 entries large_dir_threshold_entries: 1000, // 1000 entries
chunk_size_bytes: 1024 * 1024, // 1 MB chunk_size_bytes: 1024 * 1024, // 1 MB
max_in_memory_file_size_mb: 50, // 50 MB max_in_memory_file_size_mb: 50, // 50 MB
} }
} }
} }
@@ -118,7 +118,7 @@ impl ResourceConfig {
pub fn is_large_file(&self, size_bytes: u64) -> bool { pub fn is_large_file(&self, size_bytes: u64) -> bool {
self.bytes_to_mb(size_bytes) >= self.large_file_threshold_mb self.bytes_to_mb(size_bytes) >= self.large_file_threshold_mb
} }
/// Determines if a file is large enough for parallel processing /// Determines if a file is large enough for parallel processing
pub fn needs_parallel_processing(&self, size_bytes: u64, config: &ConcurrencyConfig) -> bool { pub fn needs_parallel_processing(&self, size_bytes: u64, config: &ConcurrencyConfig) -> bool {
self.bytes_to_mb(size_bytes) >= config.min_size_for_parallel_chunks_mb self.bytes_to_mb(size_bytes) >= config.min_size_for_parallel_chunks_mb
@@ -133,27 +133,27 @@ impl ResourceConfig {
pub fn is_large_directory(&self, entry_count: usize) -> bool { pub fn is_large_directory(&self, entry_count: usize) -> bool {
entry_count >= self.large_dir_threshold_entries entry_count >= self.large_dir_threshold_entries
} }
/// Calculates the number of chunks for parallel processing /// Calculates the number of chunks for parallel processing
pub fn calculate_optimal_chunks(&self, size_bytes: u64, config: &ConcurrencyConfig) -> usize { pub fn calculate_optimal_chunks(&self, size_bytes: u64, config: &ConcurrencyConfig) -> usize {
// If the file is not large enough, return 1 // If the file is not large enough, return 1
if !self.needs_parallel_processing(size_bytes, config) { if !self.needs_parallel_processing(size_bytes, config) {
return 1; return 1;
} }
// Calculate the number of chunks based on size // Calculate the number of chunks based on size
let chunk_count = (size_bytes as usize).div_ceil(config.parallel_chunk_size_bytes); let chunk_count = (size_bytes as usize).div_ceil(config.parallel_chunk_size_bytes);
// Limit to the maximum number of parallel chunks // Limit to the maximum number of parallel chunks
chunk_count.min(config.max_parallel_chunks) chunk_count.min(config.max_parallel_chunks)
} }
/// Calculates the optimal size of each chunk for parallel processing /// Calculates the optimal size of each chunk for parallel processing
pub fn calculate_chunk_size(&self, file_size: u64, chunk_count: usize) -> usize { pub fn calculate_chunk_size(&self, file_size: u64, chunk_count: usize) -> usize {
if chunk_count <= 1 { if chunk_count <= 1 {
return file_size as usize; return file_size as usize;
} }
// Distribute the size evenly among the chunks // Distribute the size evenly among the chunks
(file_size as usize).div_ceil(chunk_count) (file_size as usize).div_ceil(chunk_count)
} }
@@ -183,7 +183,7 @@ impl Default for ConcurrencyConfig {
max_concurrent_dirs: 5, max_concurrent_dirs: 5,
max_concurrent_io: 20, max_concurrent_io: 20,
max_parallel_chunks: 8, max_parallel_chunks: 8,
min_size_for_parallel_chunks_mb: 200, // 200 MB min_size_for_parallel_chunks_mb: 200, // 200 MB
parallel_chunk_size_bytes: 8 * 1024 * 1024, // 8 MB parallel_chunk_size_bytes: 8 * 1024 * 1024, // 8 MB
} }
} }
@@ -206,9 +206,9 @@ impl Default for StorageConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
root_dir: "storage".to_string(), root_dir: "storage".to_string(),
chunk_size: 1024 * 1024, // 1 MB chunk_size: 1024 * 1024, // 1 MB
parallel_threshold: 100 * 1024 * 1024, // 100 MB parallel_threshold: 100 * 1024 * 1024, // 100 MB
trash_retention_days: 30, // 30 days trash_retention_days: 30, // 30 days
} }
} }
} }
@@ -255,9 +255,9 @@ impl Default for AuthConfig {
// to set OXICLOUD_JWT_SECRET in production. The from_env() method // to set OXICLOUD_JWT_SECRET in production. The from_env() method
// will validate this and warn/panic if not configured. // will validate this and warn/panic if not configured.
jwt_secret: String::new(), jwt_secret: String::new(),
access_token_expiry_secs: 3600, // 1 hour access_token_expiry_secs: 3600, // 1 hour
refresh_token_expiry_secs: 2592000, // 30 days refresh_token_expiry_secs: 2592000, // 30 days
hash_memory_cost: 65536, // 64MB hash_memory_cost: 65536, // 64MB
hash_time_cost: 3, hash_time_cost: 3,
} }
} }
@@ -316,20 +316,36 @@ impl OidcConfig {
if let Ok(v) = env::var("OXICLOUD_OIDC_ENABLED") { if let Ok(v) = env::var("OXICLOUD_OIDC_ENABLED") {
cfg.enabled = v.parse::<bool>().unwrap_or(false); cfg.enabled = v.parse::<bool>().unwrap_or(false);
} }
if let Ok(v) = env::var("OXICLOUD_OIDC_ISSUER_URL") { cfg.issuer_url = v; } if let Ok(v) = env::var("OXICLOUD_OIDC_ISSUER_URL") {
if let Ok(v) = env::var("OXICLOUD_OIDC_CLIENT_ID") { cfg.client_id = v; } cfg.issuer_url = v;
if let Ok(v) = env::var("OXICLOUD_OIDC_CLIENT_SECRET") { cfg.client_secret = v; } }
if let Ok(v) = env::var("OXICLOUD_OIDC_REDIRECT_URI") { cfg.redirect_uri = v; } if let Ok(v) = env::var("OXICLOUD_OIDC_CLIENT_ID") {
if let Ok(v) = env::var("OXICLOUD_OIDC_SCOPES") { cfg.scopes = v; } cfg.client_id = v;
if let Ok(v) = env::var("OXICLOUD_OIDC_FRONTEND_URL") { cfg.frontend_url = v; } }
if let Ok(v) = env::var("OXICLOUD_OIDC_CLIENT_SECRET") {
cfg.client_secret = v;
}
if let Ok(v) = env::var("OXICLOUD_OIDC_REDIRECT_URI") {
cfg.redirect_uri = v;
}
if let Ok(v) = env::var("OXICLOUD_OIDC_SCOPES") {
cfg.scopes = v;
}
if let Ok(v) = env::var("OXICLOUD_OIDC_FRONTEND_URL") {
cfg.frontend_url = v;
}
if let Ok(v) = env::var("OXICLOUD_OIDC_AUTO_PROVISION") { if let Ok(v) = env::var("OXICLOUD_OIDC_AUTO_PROVISION") {
cfg.auto_provision = v.parse::<bool>().unwrap_or(true); cfg.auto_provision = v.parse::<bool>().unwrap_or(true);
} }
if let Ok(v) = env::var("OXICLOUD_OIDC_ADMIN_GROUPS") { cfg.admin_groups = v; } if let Ok(v) = env::var("OXICLOUD_OIDC_ADMIN_GROUPS") {
cfg.admin_groups = v;
}
if let Ok(v) = env::var("OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN") { if let Ok(v) = env::var("OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN") {
cfg.disable_password_login = v.parse::<bool>().unwrap_or(false); cfg.disable_password_login = v.parse::<bool>().unwrap_or(false);
} }
if let Ok(v) = env::var("OXICLOUD_OIDC_PROVIDER_NAME") { cfg.provider_name = v; } if let Ok(v) = env::var("OXICLOUD_OIDC_PROVIDER_NAME") {
cfg.provider_name = v;
}
cfg cfg
} }
} }
@@ -347,11 +363,11 @@ pub struct FeaturesConfig {
impl Default for FeaturesConfig { impl Default for FeaturesConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
enable_auth: true, // Enable authentication by default enable_auth: true, // Enable authentication by default
enable_user_storage_quotas: false, enable_user_storage_quotas: false,
enable_file_sharing: true, // Enable file sharing by default enable_file_sharing: true, // Enable file sharing by default
enable_trash: true, // Enable trash feature enable_trash: true, // Enable trash feature
enable_search: true, // Enable search feature enable_search: true, // Enable search feature
} }
} }
} }
@@ -410,47 +426,50 @@ impl Default for AppConfig {
impl AppConfig { impl AppConfig {
pub fn from_env() -> Self { pub fn from_env() -> Self {
let mut config = Self::default(); let mut config = Self::default();
// Use environment variables to override default values // Use environment variables to override default values
if let Ok(storage_path) = env::var("OXICLOUD_STORAGE_PATH") { if let Ok(storage_path) = env::var("OXICLOUD_STORAGE_PATH") {
config.storage_path = PathBuf::from(storage_path); config.storage_path = PathBuf::from(storage_path);
} }
if let Ok(static_path) = env::var("OXICLOUD_STATIC_PATH") { if let Ok(static_path) = env::var("OXICLOUD_STATIC_PATH") {
config.static_path = PathBuf::from(static_path); config.static_path = PathBuf::from(static_path);
} }
if let Ok(server_port) = env::var("OXICLOUD_SERVER_PORT") if let Ok(server_port) = env::var("OXICLOUD_SERVER_PORT")
&& let Ok(port) = server_port.parse::<u16>() { && let Ok(port) = server_port.parse::<u16>()
config.server_port = port; {
} config.server_port = port;
}
if let Ok(server_host) = env::var("OXICLOUD_SERVER_HOST") { if let Ok(server_host) = env::var("OXICLOUD_SERVER_HOST") {
config.server_host = server_host; config.server_host = server_host;
} }
// Database configuration // Database configuration
if let Ok(connection_string) = env::var("OXICLOUD_DB_CONNECTION_STRING") { if let Ok(connection_string) = env::var("OXICLOUD_DB_CONNECTION_STRING") {
config.database.connection_string = connection_string; config.database.connection_string = connection_string;
} }
if let Ok(max_connections) = env::var("OXICLOUD_DB_MAX_CONNECTIONS") if let Ok(max_connections) =
.map(|v| v.parse::<u32>()) env::var("OXICLOUD_DB_MAX_CONNECTIONS").map(|v| v.parse::<u32>())
&& let Ok(val) = max_connections { && let Ok(val) = max_connections
config.database.max_connections = val; {
} config.database.max_connections = val;
}
if let Ok(min_connections) = env::var("OXICLOUD_DB_MIN_CONNECTIONS")
.map(|v| v.parse::<u32>()) if let Ok(min_connections) =
&& let Ok(val) = min_connections { env::var("OXICLOUD_DB_MIN_CONNECTIONS").map(|v| v.parse::<u32>())
config.database.min_connections = val; && let Ok(val) = min_connections
} {
config.database.min_connections = val;
}
// Auth configuration // Auth configuration
if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") { if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") {
config.auth.jwt_secret = jwt_secret; config.auth.jwt_secret = jwt_secret;
} }
// SECURITY: Validate JWT secret when auth is enabled // SECURITY: Validate JWT secret when auth is enabled
if config.features.enable_auth && config.auth.jwt_secret.is_empty() { if config.features.enable_auth && config.auth.jwt_secret.is_empty() {
// Generate a random secret for this session and warn loudly // Generate a random secret for this session and warn loudly
@@ -459,7 +478,7 @@ impl AppConfig {
OsRng.fill_bytes(&mut key); OsRng.fill_bytes(&mut key);
let generated_secret: String = key.iter().map(|b| format!("{:02x}", b)).collect(); let generated_secret: String = key.iter().map(|b| format!("{:02x}", b)).collect();
config.auth.jwt_secret = generated_secret; config.auth.jwt_secret = generated_secret;
tracing::warn!("=========================================================="); tracing::warn!("==========================================================");
tracing::warn!("OXICLOUD_JWT_SECRET is not set."); tracing::warn!("OXICLOUD_JWT_SECRET is not set.");
tracing::warn!("A random secret has been generated for this session."); tracing::warn!("A random secret has been generated for this session.");
@@ -467,50 +486,54 @@ impl AppConfig {
tracing::warn!("Set OXICLOUD_JWT_SECRET env var for production use."); tracing::warn!("Set OXICLOUD_JWT_SECRET env var for production use.");
tracing::warn!("=========================================================="); tracing::warn!("==========================================================");
} }
if let Ok(access_token_expiry) = env::var("OXICLOUD_ACCESS_TOKEN_EXPIRY_SECS") if let Ok(access_token_expiry) =
.map(|v| v.parse::<i64>()) env::var("OXICLOUD_ACCESS_TOKEN_EXPIRY_SECS").map(|v| v.parse::<i64>())
&& let Ok(val) = access_token_expiry { && let Ok(val) = access_token_expiry
config.auth.access_token_expiry_secs = val; {
} config.auth.access_token_expiry_secs = val;
}
if let Ok(refresh_token_expiry) = env::var("OXICLOUD_REFRESH_TOKEN_EXPIRY_SECS")
.map(|v| v.parse::<i64>()) if let Ok(refresh_token_expiry) =
&& let Ok(val) = refresh_token_expiry { env::var("OXICLOUD_REFRESH_TOKEN_EXPIRY_SECS").map(|v| v.parse::<i64>())
config.auth.refresh_token_expiry_secs = val; && let Ok(val) = refresh_token_expiry
} {
config.auth.refresh_token_expiry_secs = val;
}
// Feature flags // Feature flags
if let Ok(enable_auth) = env::var("OXICLOUD_ENABLE_AUTH") if let Ok(enable_auth) = env::var("OXICLOUD_ENABLE_AUTH").map(|v| v.parse::<bool>())
.map(|v| v.parse::<bool>()) && let Ok(val) = enable_auth
&& let Ok(val) = enable_auth { {
config.features.enable_auth = val; config.features.enable_auth = val;
} }
if let Ok(enable_user_storage_quotas) = env::var("OXICLOUD_ENABLE_USER_STORAGE_QUOTAS") if let Ok(enable_user_storage_quotas) =
.map(|v| v.parse::<bool>()) env::var("OXICLOUD_ENABLE_USER_STORAGE_QUOTAS").map(|v| v.parse::<bool>())
&& let Ok(val) = enable_user_storage_quotas { && let Ok(val) = enable_user_storage_quotas
config.features.enable_user_storage_quotas = val; {
} config.features.enable_user_storage_quotas = val;
}
if let Ok(enable_file_sharing) = env::var("OXICLOUD_ENABLE_FILE_SHARING")
.map(|v| v.parse::<bool>()) if let Ok(enable_file_sharing) =
&& let Ok(val) = enable_file_sharing { env::var("OXICLOUD_ENABLE_FILE_SHARING").map(|v| v.parse::<bool>())
config.features.enable_file_sharing = val; && let Ok(val) = enable_file_sharing
} {
config.features.enable_file_sharing = val;
if let Ok(enable_trash) = env::var("OXICLOUD_ENABLE_TRASH") }
.map(|v| v.parse::<bool>())
&& let Ok(val) = enable_trash { if let Ok(enable_trash) = env::var("OXICLOUD_ENABLE_TRASH").map(|v| v.parse::<bool>())
config.features.enable_trash = val; && let Ok(val) = enable_trash
} {
config.features.enable_trash = val;
if let Ok(enable_search) = env::var("OXICLOUD_ENABLE_SEARCH") }
.map(|v| v.parse::<bool>())
&& let Ok(val) = enable_search { if let Ok(enable_search) = env::var("OXICLOUD_ENABLE_SEARCH").map(|v| v.parse::<bool>())
config.features.enable_search = val; && let Ok(val) = enable_search
} {
config.features.enable_search = val;
}
// OIDC configuration // OIDC configuration
if let Ok(v) = env::var("OXICLOUD_OIDC_ENABLED") { if let Ok(v) = env::var("OXICLOUD_OIDC_ENABLED") {
config.oidc.enabled = v.parse::<bool>().unwrap_or(false); config.oidc.enabled = v.parse::<bool>().unwrap_or(false);
@@ -548,23 +571,28 @@ impl AppConfig {
// Validate OIDC config when enabled // Validate OIDC config when enabled
if config.oidc.enabled if config.oidc.enabled
&& (config.oidc.issuer_url.is_empty() || config.oidc.client_id.is_empty() || config.oidc.client_secret.is_empty()) { && (config.oidc.issuer_url.is_empty()
tracing::error!("OIDC is enabled but OXICLOUD_OIDC_ISSUER_URL, OXICLOUD_OIDC_CLIENT_ID, or OXICLOUD_OIDC_CLIENT_SECRET are not set"); || config.oidc.client_id.is_empty()
config.oidc.enabled = false; || config.oidc.client_secret.is_empty())
} {
tracing::error!(
"OIDC is enabled but OXICLOUD_OIDC_ISSUER_URL, OXICLOUD_OIDC_CLIENT_ID, or OXICLOUD_OIDC_CLIENT_SECRET are not set"
);
config.oidc.enabled = false;
}
config config
} }
pub fn with_features(mut self, features: FeaturesConfig) -> Self { pub fn with_features(mut self, features: FeaturesConfig) -> Self {
self.features = features; self.features = features;
self self
} }
pub fn db_enabled(&self) -> bool { pub fn db_enabled(&self) -> bool {
self.features.enable_auth self.features.enable_auth
} }
pub fn auth_enabled(&self) -> bool { pub fn auth_enabled(&self) -> bool {
self.features.enable_auth self.features.enable_auth
} }
@@ -595,4 +623,4 @@ impl AppConfig {
/// Gets a default global configuration /// Gets a default global configuration
pub fn default_config() -> AppConfig { pub fn default_config() -> AppConfig {
AppConfig::default() AppConfig::default()
} }
+330 -252
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,4 +1,4 @@
pub mod errors;
pub mod config; pub mod config;
pub mod di; pub mod di;
pub mod stubs; pub mod errors;
pub mod stubs;
+55 -106
View File
@@ -14,23 +14,25 @@ use bytes::Bytes;
use futures::Stream; use futures::Stream;
use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::{CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto}; use crate::application::dtos::folder_dto::{
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
};
use crate::application::dtos::pagination::{PaginatedResponseDto, PaginationRequestDto}; use crate::application::dtos::pagination::{PaginatedResponseDto, PaginationRequestDto};
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto}; use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
use crate::application::ports::compression_ports::{CompressionLevel, CompressionPort}; use crate::application::ports::compression_ports::{CompressionLevel, CompressionPort};
use crate::application::ports::file_ports::{ use crate::application::ports::file_ports::{
FileManagementUseCase, FileRetrievalUseCase, FileUploadUseCase, FileUseCaseFactory, FileManagementUseCase, FileRetrievalUseCase, FileUploadUseCase, FileUseCaseFactory,
UploadStrategy, OptimizedFileContent, OptimizedFileContent, UploadStrategy,
}; };
use crate::application::ports::inbound::{FolderUseCase, SearchUseCase}; use crate::application::ports::inbound::{FolderUseCase, SearchUseCase};
use crate::application::ports::outbound::IdMappingPort; use crate::application::ports::outbound::IdMappingPort;
use crate::domain::repositories::folder_repository::FolderRepository;
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort}; use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::application::ports::zip_ports::ZipPort; use crate::application::ports::zip_ports::ZipPort;
use crate::application::services::storage_mediator::{StorageMediator, StorageMediatorError}; use crate::application::services::storage_mediator::{StorageMediator, StorageMediatorError};
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use crate::domain::entities::file::File; use crate::domain::entities::file::File;
use crate::domain::entities::folder::Folder; use crate::domain::entities::folder::Folder;
use crate::domain::repositories::folder_repository::FolderRepository;
use crate::domain::services::i18n_service::{I18nResult, I18nService, Locale}; use crate::domain::services::i18n_service::{I18nResult, I18nService, Locale};
use crate::domain::services::path_service::StoragePath; use crate::domain::services::path_service::StoragePath;
@@ -89,10 +91,7 @@ pub struct StubIdMappingService;
#[async_trait] #[async_trait]
impl IdMappingPort for StubIdMappingService { impl IdMappingPort for StubIdMappingService {
async fn get_or_create_id( async fn get_or_create_id(&self, _path: &StoragePath) -> Result<String, DomainError> {
&self,
_path: &StoragePath,
) -> Result<String, DomainError> {
Ok("dummy-id".to_string()) Ok("dummy-id".to_string())
} }
@@ -100,11 +99,7 @@ impl IdMappingPort for StubIdMappingService {
Ok(StoragePath::from_string("/")) Ok(StoragePath::from_string("/"))
} }
async fn update_path( async fn update_path(&self, _id: &str, _new_path: &StoragePath) -> Result<(), DomainError> {
&self,
_id: &str,
_new_path: &StoragePath,
) -> Result<(), DomainError> {
Ok(()) Ok(())
} }
@@ -125,10 +120,7 @@ pub struct StubStorageMediator;
#[async_trait] #[async_trait]
impl StorageMediator for StubStorageMediator { impl StorageMediator for StubStorageMediator {
async fn get_folder_path( async fn get_folder_path(&self, _folder_id: &str) -> Result<PathBuf, StorageMediatorError> {
&self,
_folder_id: &str,
) -> Result<PathBuf, StorageMediatorError> {
Ok(PathBuf::from("/tmp")) Ok(PathBuf::from("/tmp"))
} }
@@ -139,19 +131,13 @@ impl StorageMediator for StubStorageMediator {
Ok(StoragePath::root()) Ok(StoragePath::root())
} }
async fn get_folder( async fn get_folder(&self, _folder_id: &str) -> Result<Folder, StorageMediatorError> {
&self,
_folder_id: &str,
) -> Result<Folder, StorageMediatorError> {
Err(StorageMediatorError::NotFound( Err(StorageMediatorError::NotFound(
"Stub not implemented".to_string(), "Stub not implemented".to_string(),
)) ))
} }
async fn file_exists_at_path( async fn file_exists_at_path(&self, _path: &Path) -> Result<bool, StorageMediatorError> {
&self,
_path: &Path,
) -> Result<bool, StorageMediatorError> {
Ok(false) Ok(false)
} }
@@ -162,10 +148,7 @@ impl StorageMediator for StubStorageMediator {
Ok(false) Ok(false)
} }
async fn folder_exists_at_path( async fn folder_exists_at_path(&self, _path: &Path) -> Result<bool, StorageMediatorError> {
&self,
_path: &Path,
) -> Result<bool, StorageMediatorError> {
Ok(false) Ok(false)
} }
@@ -184,10 +167,7 @@ impl StorageMediator for StubStorageMediator {
PathBuf::from("/tmp") PathBuf::from("/tmp")
} }
async fn ensure_directory( async fn ensure_directory(&self, _path: &Path) -> Result<(), StorageMediatorError> {
&self,
_path: &Path,
) -> Result<(), StorageMediatorError> {
Ok(()) Ok(())
} }
@@ -236,10 +216,7 @@ impl FileReadPort for StubFileReadPort {
Ok(File::default()) Ok(File::default())
} }
async fn list_files( async fn list_files(&self, _folder_id: Option<&str>) -> Result<Vec<File>, DomainError> {
&self,
_folder_id: Option<&str>,
) -> Result<Vec<File>, DomainError> {
Ok(Vec::new()) Ok(Vec::new())
} }
@@ -314,11 +291,7 @@ impl FileWritePort for StubFileWritePort {
Ok(File::default()) Ok(File::default())
} }
async fn rename_file( async fn rename_file(&self, _file_id: &str, _new_name: &str) -> Result<File, DomainError> {
&self,
_file_id: &str,
_new_name: &str,
) -> Result<File, DomainError> {
Ok(File::default()) Ok(File::default())
} }
@@ -348,7 +321,11 @@ impl FileWritePort for StubFileWritePort {
Ok(()) Ok(())
} }
async fn restore_from_trash(&self, _file_id: &str, _original_path: &str) -> Result<(), DomainError> { async fn restore_from_trash(
&self,
_file_id: &str,
_original_path: &str,
) -> Result<(), DomainError> {
Ok(()) Ok(())
} }
@@ -377,17 +354,11 @@ impl FolderRepository for StubFolderStoragePort {
Ok(Folder::default()) Ok(Folder::default())
} }
async fn get_folder_by_path( async fn get_folder_by_path(&self, _storage_path: &StoragePath) -> Result<Folder, DomainError> {
&self,
_storage_path: &StoragePath,
) -> Result<Folder, DomainError> {
Ok(Folder::default()) Ok(Folder::default())
} }
async fn list_folders( async fn list_folders(&self, _parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError> {
&self,
_parent_id: Option<&str>,
) -> Result<Vec<Folder>, DomainError> {
Ok(Vec::new()) Ok(Vec::new())
} }
@@ -401,11 +372,7 @@ impl FolderRepository for StubFolderStoragePort {
Ok((Vec::new(), Some(0))) Ok((Vec::new(), Some(0)))
} }
async fn rename_folder( async fn rename_folder(&self, _id: &str, _new_name: String) -> Result<Folder, DomainError> {
&self,
_id: &str,
_new_name: String,
) -> Result<Folder, DomainError> {
Ok(Folder::default()) Ok(Folder::default())
} }
@@ -421,17 +388,11 @@ impl FolderRepository for StubFolderStoragePort {
Ok(()) Ok(())
} }
async fn folder_exists( async fn folder_exists(&self, _storage_path: &StoragePath) -> Result<bool, DomainError> {
&self,
_storage_path: &StoragePath,
) -> Result<bool, DomainError> {
Ok(false) Ok(false)
} }
async fn get_folder_path( async fn get_folder_path(&self, _id: &str) -> Result<StoragePath, DomainError> {
&self,
_id: &str,
) -> Result<StoragePath, DomainError> {
Ok(StoragePath::from_string("/")) Ok(StoragePath::from_string("/"))
} }
@@ -439,7 +400,11 @@ impl FolderRepository for StubFolderStoragePort {
Ok(()) Ok(())
} }
async fn restore_from_trash(&self, _folder_id: &str, _original_path: &str) -> Result<(), DomainError> { async fn restore_from_trash(
&self,
_folder_id: &str,
_original_path: &str,
) -> Result<(), DomainError> {
Ok(()) Ok(())
} }
@@ -481,10 +446,7 @@ pub struct StubFolderUseCase;
#[async_trait] #[async_trait]
impl FolderUseCase for StubFolderUseCase { impl FolderUseCase for StubFolderUseCase {
async fn create_folder( async fn create_folder(&self, _dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
&self,
_dto: CreateFolderDto,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default()) Ok(FolderDto::default())
} }
@@ -492,17 +454,11 @@ impl FolderUseCase for StubFolderUseCase {
Ok(FolderDto::default()) Ok(FolderDto::default())
} }
async fn get_folder_by_path( async fn get_folder_by_path(&self, _path: &str) -> Result<FolderDto, DomainError> {
&self,
_path: &str,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default()) Ok(FolderDto::default())
} }
async fn list_folders( async fn list_folders(&self, _parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> {
&self,
_parent_id: Option<&str>,
) -> Result<Vec<FolderDto>, DomainError> {
Ok(Vec::new()) Ok(Vec::new())
} }
@@ -522,11 +478,7 @@ impl FolderUseCase for StubFolderUseCase {
Ok(FolderDto::default()) Ok(FolderDto::default())
} }
async fn move_folder( async fn move_folder(&self, _id: &str, _dto: MoveFolderDto) -> Result<FolderDto, DomainError> {
&self,
_id: &str,
_dto: MoveFolderDto,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default()) Ok(FolderDto::default())
} }
@@ -564,7 +516,13 @@ impl FileUploadUseCase for StubFileUploadUseCase {
Ok((FileDto::default(), UploadStrategy::Buffered)) Ok((FileDto::default(), UploadStrategy::Buffered))
} }
async fn create_file(&self, _parent_path: &str, _filename: &str, _content: &[u8], _content_type: &str) -> Result<FileDto, DomainError> { async fn create_file(
&self,
_parent_path: &str,
_filename: &str,
_content: &[u8],
_content_type: &str,
) -> Result<FileDto, DomainError> {
Ok(FileDto::default()) Ok(FileDto::default())
} }
@@ -585,10 +543,7 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
Ok(FileDto::default()) Ok(FileDto::default())
} }
async fn list_files( async fn list_files(&self, _folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError> {
&self,
_folder_id: Option<&str>,
) -> Result<Vec<FileDto>, DomainError> {
Ok(Vec::new()) Ok(Vec::new())
} }
@@ -610,11 +565,14 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
_accept_webp: bool, _accept_webp: bool,
_prefer_original: bool, _prefer_original: bool,
) -> Result<(FileDto, OptimizedFileContent), DomainError> { ) -> Result<(FileDto, OptimizedFileContent), DomainError> {
Ok((FileDto::default(), OptimizedFileContent::Bytes { Ok((
data: Bytes::new(), FileDto::default(),
mime_type: String::new(), OptimizedFileContent::Bytes {
was_transcoded: false, data: Bytes::new(),
})) mime_type: String::new(),
was_transcoded: false,
},
))
} }
async fn get_file_range_stream( async fn get_file_range_stream(
@@ -648,11 +606,7 @@ impl FileManagementUseCase for StubFileManagementUseCase {
Ok(FileDto::default()) Ok(FileDto::default())
} }
async fn rename_file( async fn rename_file(&self, _file_id: &str, _new_name: &str) -> Result<FileDto, DomainError> {
&self,
_file_id: &str,
_new_name: &str,
) -> Result<FileDto, DomainError> {
Ok(FileDto::default()) Ok(FileDto::default())
} }
@@ -660,11 +614,7 @@ impl FileManagementUseCase for StubFileManagementUseCase {
Ok(()) Ok(())
} }
async fn delete_with_cleanup( async fn delete_with_cleanup(&self, _id: &str, _user_id: &str) -> Result<bool, DomainError> {
&self,
_id: &str,
_user_id: &str,
) -> Result<bool, DomainError> {
Ok(false) Ok(false)
} }
} }
@@ -697,10 +647,7 @@ pub struct StubSearchUseCase;
#[async_trait] #[async_trait]
impl SearchUseCase for StubSearchUseCase { impl SearchUseCase for StubSearchUseCase {
async fn search( async fn search(&self, _criteria: SearchCriteriaDto) -> Result<SearchResultsDto, DomainError> {
&self,
_criteria: SearchCriteriaDto,
) -> Result<SearchResultsDto, DomainError> {
Ok(SearchResultsDto::empty()) Ok(SearchResultsDto::empty())
} }
@@ -713,7 +660,9 @@ impl SearchUseCase for StubSearchUseCase {
// MetadataCachePort // MetadataCachePort
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
use crate::application::ports::cache_ports::{MetadataCachePort, CachedMetadataDto, ContentCachePort}; use crate::application::ports::cache_ports::{
CachedMetadataDto, ContentCachePort, MetadataCachePort,
};
pub struct StubMetadataCachePort; pub struct StubMetadataCachePort;
+50 -51
View File
@@ -1,25 +1,24 @@
use chrono::{DateTime, Utc};
/** /**
* Calendar Entity * Calendar Entity
* *
* This module defines the Calendar entity, which represents a calendar in the CalDAV * This module defines the Calendar entity, which represents a calendar in the CalDAV
* implementation. Calendars contain calendar events and are owned by users. * implementation. Calendars contain calendar events and are owned by users.
* *
* Calendars have properties such as name, color, and description, and they serve as * Calendars have properties such as name, color, and description, and they serve as
* containers for calendar events. Each calendar belongs to a specific user and can * containers for calendar events. Each calendar belongs to a specific user and can
* have custom properties. * have custom properties.
*/ */
use uuid::Uuid; use uuid::Uuid;
use chrono::{DateTime, Utc};
use crate::common::errors::{Result, DomainError, ErrorKind}; use crate::common::errors::{DomainError, ErrorKind, Result};
// Re-export entity errors from the centralized module // Re-export entity errors from the centralized module
pub use super::entity_errors::CalendarError; pub use super::entity_errors::CalendarError;
/** /**
* Calendar entity. * Calendar entity.
* *
* Represents a calendar container that can hold multiple calendar events. * Represents a calendar container that can hold multiple calendar events.
* Each calendar is owned by a user and has properties like name, color, and description. * Each calendar is owned by a user and has properties like name, color, and description.
*/ */
@@ -27,25 +26,25 @@ pub use super::entity_errors::CalendarError;
pub struct Calendar { pub struct Calendar {
/// Unique identifier for the calendar /// Unique identifier for the calendar
id: Uuid, id: Uuid,
/// Display name of the calendar /// Display name of the calendar
name: String, name: String,
/// ID of the user who owns this calendar /// ID of the user who owns this calendar
owner_id: String, owner_id: String,
/// Optional description of the calendar /// Optional description of the calendar
description: Option<String>, description: Option<String>,
/// Optional color code for UI display (hex format #RRGGBB) /// Optional color code for UI display (hex format #RRGGBB)
color: Option<String>, color: Option<String>,
/// Time when the calendar was created /// Time when the calendar was created
created_at: DateTime<Utc>, created_at: DateTime<Utc>,
/// Time when the calendar was last modified /// Time when the calendar was last modified
updated_at: DateTime<Utc>, updated_at: DateTime<Utc>,
/// Optional list of custom properties (for extended CalDAV support) /// Optional list of custom properties (for extended CalDAV support)
custom_properties: std::collections::HashMap<String, String>, custom_properties: std::collections::HashMap<String, String>,
} }
@@ -53,7 +52,7 @@ pub struct Calendar {
impl Calendar { impl Calendar {
/** /**
* Creates a new calendar with the given properties. * Creates a new calendar with the given properties.
* *
* @param name Display name of the calendar * @param name Display name of the calendar
* @param owner_id ID of the user who owns this calendar * @param owner_id ID of the user who owns this calendar
* @param description Optional description of the calendar * @param description Optional description of the calendar
@@ -74,7 +73,7 @@ impl Calendar {
"Calendar name cannot be empty", "Calendar name cannot be empty",
)); ));
} }
if owner_id.is_empty() { if owner_id.is_empty() {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
@@ -82,7 +81,7 @@ impl Calendar {
"Owner ID cannot be empty", "Owner ID cannot be empty",
)); ));
} }
// Validate color format if provided (#RRGGBB) // Validate color format if provided (#RRGGBB)
if let Some(ref color_str) = color { if let Some(ref color_str) = color {
if !color_str.starts_with('#') || color_str.len() != 7 { if !color_str.starts_with('#') || color_str.len() != 7 {
@@ -92,7 +91,7 @@ impl Calendar {
"Color must be in #RRGGBB format", "Color must be in #RRGGBB format",
)); ));
} }
// Check if remaining characters are valid hex // Check if remaining characters are valid hex
if color_str[1..].chars().any(|c| !c.is_ascii_hexdigit()) { if color_str[1..].chars().any(|c| !c.is_ascii_hexdigit()) {
return Err(DomainError::new( return Err(DomainError::new(
@@ -102,9 +101,9 @@ impl Calendar {
)); ));
} }
} }
let now = Utc::now(); let now = Utc::now();
Ok(Self { Ok(Self {
id: Uuid::new_v4(), id: Uuid::new_v4(),
name, name,
@@ -116,11 +115,11 @@ impl Calendar {
custom_properties: std::collections::HashMap::new(), custom_properties: std::collections::HashMap::new(),
}) })
} }
/** /**
* Creates a calendar with specific ID and timestamps. * Creates a calendar with specific ID and timestamps.
* Typically used when reconstructing from storage. * Typically used when reconstructing from storage.
* *
* @param id Unique identifier for the calendar * @param id Unique identifier for the calendar
* @param name Display name of the calendar * @param name Display name of the calendar
* @param owner_id ID of the user who owns this calendar * @param owner_id ID of the user who owns this calendar
@@ -147,7 +146,7 @@ impl Calendar {
"Calendar name cannot be empty", "Calendar name cannot be empty",
)); ));
} }
if owner_id.is_empty() { if owner_id.is_empty() {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
@@ -155,7 +154,7 @@ impl Calendar {
"Owner ID cannot be empty", "Owner ID cannot be empty",
)); ));
} }
Ok(Self { Ok(Self {
id, id,
name, name,
@@ -167,59 +166,59 @@ impl Calendar {
custom_properties: std::collections::HashMap::new(), custom_properties: std::collections::HashMap::new(),
}) })
} }
// Getters // Getters
/// Returns the calendar's unique identifier /// Returns the calendar's unique identifier
pub fn id(&self) -> &Uuid { pub fn id(&self) -> &Uuid {
&self.id &self.id
} }
/// Returns the calendar's display name /// Returns the calendar's display name
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }
/// Returns the ID of the user who owns this calendar /// Returns the ID of the user who owns this calendar
pub fn owner_id(&self) -> &str { pub fn owner_id(&self) -> &str {
&self.owner_id &self.owner_id
} }
/// Returns the calendar's description, if any /// Returns the calendar's description, if any
pub fn description(&self) -> Option<&str> { pub fn description(&self) -> Option<&str> {
self.description.as_deref() self.description.as_deref()
} }
/// Returns the calendar's color code, if any /// Returns the calendar's color code, if any
pub fn color(&self) -> Option<&str> { pub fn color(&self) -> Option<&str> {
self.color.as_deref() self.color.as_deref()
} }
/// Returns the time when the calendar was created /// Returns the time when the calendar was created
pub fn created_at(&self) -> &DateTime<Utc> { pub fn created_at(&self) -> &DateTime<Utc> {
&self.created_at &self.created_at
} }
/// Returns the time when the calendar was last modified /// Returns the time when the calendar was last modified
pub fn updated_at(&self) -> &DateTime<Utc> { pub fn updated_at(&self) -> &DateTime<Utc> {
&self.updated_at &self.updated_at
} }
/// Returns a custom property value by name, if it exists /// Returns a custom property value by name, if it exists
pub fn custom_property(&self, name: &str) -> Option<&str> { pub fn custom_property(&self, name: &str) -> Option<&str> {
self.custom_properties.get(name).map(|s| s.as_str()) self.custom_properties.get(name).map(|s| s.as_str())
} }
/// Returns all custom properties /// Returns all custom properties
pub fn custom_properties(&self) -> &std::collections::HashMap<String, String> { pub fn custom_properties(&self) -> &std::collections::HashMap<String, String> {
&self.custom_properties &self.custom_properties
} }
// Setters and Mutators // Setters and Mutators
/** /**
* Updates the calendar's name. * Updates the calendar's name.
* *
* @param name New display name for the calendar * @param name New display name for the calendar
* @return Result indicating success or containing a domain error * @return Result indicating success or containing a domain error
*/ */
@@ -231,25 +230,25 @@ impl Calendar {
"Calendar name cannot be empty", "Calendar name cannot be empty",
)); ));
} }
self.name = name; self.name = name;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
Ok(()) Ok(())
} }
/** /**
* Updates the calendar's description. * Updates the calendar's description.
* *
* @param description New description for the calendar * @param description New description for the calendar
*/ */
pub fn update_description(&mut self, description: Option<String>) { pub fn update_description(&mut self, description: Option<String>) {
self.description = description; self.description = description;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
/** /**
* Updates the calendar's color. * Updates the calendar's color.
* *
* @param color New color code for the calendar * @param color New color code for the calendar
* @return Result indicating success or containing a domain error * @return Result indicating success or containing a domain error
*/ */
@@ -263,7 +262,7 @@ impl Calendar {
"Color must be in #RRGGBB format", "Color must be in #RRGGBB format",
)); ));
} }
// Check if remaining characters are valid hex // Check if remaining characters are valid hex
if color_str[1..].chars().any(|c| !c.is_ascii_hexdigit()) { if color_str[1..].chars().any(|c| !c.is_ascii_hexdigit()) {
return Err(DomainError::new( return Err(DomainError::new(
@@ -273,15 +272,15 @@ impl Calendar {
)); ));
} }
} }
self.color = color; self.color = color;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
Ok(()) Ok(())
} }
/** /**
* Sets a custom property for extended CalDAV support. * Sets a custom property for extended CalDAV support.
* *
* @param name Name of the property * @param name Name of the property
* @param value Value of the property * @param value Value of the property
*/ */
@@ -289,10 +288,10 @@ impl Calendar {
self.custom_properties.insert(name, value); self.custom_properties.insert(name, value);
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
/** /**
* Removes a custom property. * Removes a custom property.
* *
* @param name Name of the property to remove * @param name Name of the property to remove
* @return true if the property was removed, false if it didn't exist * @return true if the property was removed, false if it didn't exist
*/ */
@@ -303,17 +302,17 @@ impl Calendar {
} }
result result
} }
/** /**
* Checks if this calendar belongs to the specified user. * Checks if this calendar belongs to the specified user.
* *
* @param user_id ID of the user to check ownership against * @param user_id ID of the user to check ownership against
* @return true if the calendar belongs to the user, false otherwise * @return true if the calendar belongs to the user, false otherwise
*/ */
pub fn belongs_to(&self, user_id: &str) -> bool { pub fn belongs_to(&self, user_id: &str) -> bool {
self.owner_id == user_id self.owner_id == user_id
} }
/** /**
* Updates the last modification time of the calendar to now. * Updates the last modification time of the calendar to now.
* Called when calendar events are added, modified, or removed. * Called when calendar events are added, modified, or removed.
@@ -321,4 +320,4 @@ impl Calendar {
pub fn touch(&mut self) { pub fn touch(&mut self) {
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
} }
+209 -181
View File
@@ -1,25 +1,24 @@
use chrono::{DateTime, Duration, TimeZone, Utc};
/** /**
* Calendar Event Entity * Calendar Event Entity
* *
* This module defines the CalendarEvent entity, which represents an event or * This module defines the CalendarEvent entity, which represents an event or
* appointment in a calendar, following the iCalendar (RFC 5545) specification. * appointment in a calendar, following the iCalendar (RFC 5545) specification.
* *
* Calendar events have properties like summary, description, location, start/end times, * Calendar events have properties like summary, description, location, start/end times,
* and can include recurrence rules for repeating events. Each event belongs to a * and can include recurrence rules for repeating events. Each event belongs to a
* specific calendar and stores its complete iCalendar representation. * specific calendar and stores its complete iCalendar representation.
*/ */
use uuid::Uuid; use uuid::Uuid;
use chrono::{DateTime, Utc, Duration, TimeZone};
use crate::common::errors::{Result, DomainError, ErrorKind}; use crate::common::errors::{DomainError, ErrorKind, Result};
// Re-export entity errors from the centralized module // Re-export entity errors from the centralized module
pub use super::entity_errors::CalendarEventError; pub use super::entity_errors::CalendarEventError;
/** /**
* CalendarEvent entity. * CalendarEvent entity.
* *
* Represents a calendar event or appointment that can be synced via CalDAV. * Represents a calendar event or appointment that can be synced via CalDAV.
* Follows the iCalendar format (RFC 5545) for compatibility with CalDAV clients. * Follows the iCalendar format (RFC 5545) for compatibility with CalDAV clients.
*/ */
@@ -27,40 +26,40 @@ pub use super::entity_errors::CalendarEventError;
pub struct CalendarEvent { pub struct CalendarEvent {
/// Unique identifier for the event /// Unique identifier for the event
id: Uuid, id: Uuid,
/// ID of the calendar this event belongs to /// ID of the calendar this event belongs to
calendar_id: Uuid, calendar_id: Uuid,
/// Short summary/title of the event /// Short summary/title of the event
summary: String, summary: String,
/// Detailed description of the event (optional) /// Detailed description of the event (optional)
description: Option<String>, description: Option<String>,
/// Location of the event (optional) /// Location of the event (optional)
location: Option<String>, location: Option<String>,
/// Start time of the event /// Start time of the event
start_time: DateTime<Utc>, start_time: DateTime<Utc>,
/// End time of the event /// End time of the event
end_time: DateTime<Utc>, end_time: DateTime<Utc>,
/// Whether this is an all-day event /// Whether this is an all-day event
all_day: bool, all_day: bool,
/// Recurrence rule in iCalendar RRULE format (optional) /// Recurrence rule in iCalendar RRULE format (optional)
rrule: Option<String>, rrule: Option<String>,
/// Unique identifier in iCalendar format (used for CalDAV sync) /// Unique identifier in iCalendar format (used for CalDAV sync)
ical_uid: String, ical_uid: String,
/// Complete iCalendar data (VEVENT component) /// Complete iCalendar data (VEVENT component)
ical_data: String, ical_data: String,
/// Time when the event was created /// Time when the event was created
created_at: DateTime<Utc>, created_at: DateTime<Utc>,
/// Time when the event was last modified /// Time when the event was last modified
updated_at: DateTime<Utc>, updated_at: DateTime<Utc>,
} }
@@ -68,7 +67,7 @@ pub struct CalendarEvent {
impl CalendarEvent { impl CalendarEvent {
/** /**
* Creates a new calendar event with the given properties. * Creates a new calendar event with the given properties.
* *
* @param calendar_id ID of the calendar this event belongs to * @param calendar_id ID of the calendar this event belongs to
* @param summary Short summary/title of the event * @param summary Short summary/title of the event
* @param description Detailed description of the event (optional) * @param description Detailed description of the event (optional)
@@ -99,7 +98,7 @@ impl CalendarEvent {
"Event summary cannot be empty", "Event summary cannot be empty",
)); ));
} }
if end_time < start_time { if end_time < start_time {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
@@ -107,17 +106,18 @@ impl CalendarEvent {
"End time cannot be before start time", "End time cannot be before start time",
)); ));
} }
// Validate RRULE if provided (basic validation) // Validate RRULE if provided (basic validation)
if let Some(ref rule) = rrule if let Some(ref rule) = rrule
&& !rule.starts_with("FREQ=") { && !rule.starts_with("FREQ=")
return Err(DomainError::new( {
ErrorKind::InvalidInput, return Err(DomainError::new(
"CalendarEvent", ErrorKind::InvalidInput,
"Recurrence rule must start with FREQ=", "CalendarEvent",
)); "Recurrence rule must start with FREQ=",
} ));
}
// Validate iCalendar data (basic validation) // Validate iCalendar data (basic validation)
if !ical_data.contains("BEGIN:VEVENT") || !ical_data.contains("END:VEVENT") { if !ical_data.contains("BEGIN:VEVENT") || !ical_data.contains("END:VEVENT") {
return Err(DomainError::new( return Err(DomainError::new(
@@ -126,9 +126,9 @@ impl CalendarEvent {
"iCalendar data must contain a VEVENT component", "iCalendar data must contain a VEVENT component",
)); ));
} }
let now = Utc::now(); let now = Utc::now();
Ok(Self { Ok(Self {
id: Uuid::new_v4(), id: Uuid::new_v4(),
calendar_id, calendar_id,
@@ -145,11 +145,11 @@ impl CalendarEvent {
updated_at: now, updated_at: now,
}) })
} }
/** /**
* Creates a calendar event with specific ID and timestamps. * Creates a calendar event with specific ID and timestamps.
* Typically used when reconstructing from storage. * Typically used when reconstructing from storage.
* *
* @param id Unique identifier for the event * @param id Unique identifier for the event
* @param calendar_id ID of the calendar this event belongs to * @param calendar_id ID of the calendar this event belongs to
* @param summary Short summary/title of the event * @param summary Short summary/title of the event
@@ -188,7 +188,7 @@ impl CalendarEvent {
"Event summary cannot be empty", "Event summary cannot be empty",
)); ));
} }
if end_time < start_time { if end_time < start_time {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
@@ -196,7 +196,7 @@ impl CalendarEvent {
"End time cannot be before start time", "End time cannot be before start time",
)); ));
} }
Ok(Self { Ok(Self {
id, id,
calendar_id, calendar_id,
@@ -213,11 +213,11 @@ impl CalendarEvent {
updated_at, updated_at,
}) })
} }
/** /**
* Creates a calendar event from an iCalendar VEVENT component. * Creates a calendar event from an iCalendar VEVENT component.
* Parses the iCalendar data to extract event properties. * Parses the iCalendar data to extract event properties.
* *
* @param calendar_id ID of the calendar this event belongs to * @param calendar_id ID of the calendar this event belongs to
* @param ical_data Complete iCalendar data (VEVENT component) * @param ical_data Complete iCalendar data (VEVENT component)
* @return Result containing the new CalendarEvent or a domain error * @return Result containing the new CalendarEvent or a domain error
@@ -225,58 +225,63 @@ impl CalendarEvent {
pub fn from_ical(calendar_id: Uuid, ical_data: String) -> Result<Self> { pub fn from_ical(calendar_id: Uuid, ical_data: String) -> Result<Self> {
// This implementation would require a proper iCalendar parser // This implementation would require a proper iCalendar parser
// For brevity, we're using a simplified version here // For brevity, we're using a simplified version here
// Extract required fields from iCalendar data // Extract required fields from iCalendar data
let summary = Self::extract_ical_property(&ical_data, "SUMMARY") let summary = Self::extract_ical_property(&ical_data, "SUMMARY").ok_or_else(|| {
.ok_or_else(|| DomainError::new( DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"CalendarEvent", "CalendarEvent",
"Missing SUMMARY in iCalendar data", "Missing SUMMARY in iCalendar data",
))?; )
})?;
let dtstart = Self::extract_ical_property(&ical_data, "DTSTART")
.ok_or_else(|| DomainError::new( let dtstart = Self::extract_ical_property(&ical_data, "DTSTART").ok_or_else(|| {
DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"CalendarEvent", "CalendarEvent",
"Missing DTSTART in iCalendar data", "Missing DTSTART in iCalendar data",
))?; )
})?;
let dtend = Self::extract_ical_property(&ical_data, "DTEND")
.ok_or_else(|| DomainError::new( let dtend = Self::extract_ical_property(&ical_data, "DTEND").ok_or_else(|| {
DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"CalendarEvent", "CalendarEvent",
"Missing DTEND in iCalendar data", "Missing DTEND in iCalendar data",
))?; )
})?;
// Parse dates (simplified) // Parse dates (simplified)
let start_time = Self::parse_ical_datetime(&dtstart) let start_time = Self::parse_ical_datetime(&dtstart).map_err(|e| {
.map_err(|e| DomainError::new( DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"CalendarEvent", "CalendarEvent",
format!("Invalid DTSTART: {}", e), format!("Invalid DTSTART: {}", e),
))?; )
})?;
let end_time = Self::parse_ical_datetime(&dtend)
.map_err(|e| DomainError::new( let end_time = Self::parse_ical_datetime(&dtend).map_err(|e| {
DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
"CalendarEvent", "CalendarEvent",
format!("Invalid DTEND: {}", e), format!("Invalid DTEND: {}", e),
))?; )
})?;
// Determine if all-day event (simplified check) // Determine if all-day event (simplified check)
let all_day = dtstart.contains("VALUE=DATE") && !dtstart.contains("T"); let all_day = dtstart.contains("VALUE=DATE") && !dtstart.contains("T");
// Extract optional fields // Extract optional fields
let description = Self::extract_ical_property(&ical_data, "DESCRIPTION"); let description = Self::extract_ical_property(&ical_data, "DESCRIPTION");
let location = Self::extract_ical_property(&ical_data, "LOCATION"); let location = Self::extract_ical_property(&ical_data, "LOCATION");
let rrule = Self::extract_ical_property(&ical_data, "RRULE"); let rrule = Self::extract_ical_property(&ical_data, "RRULE");
// Extract UID or generate a new one // Extract UID or generate a new one
let ical_uid = Self::extract_ical_property(&ical_data, "UID") let ical_uid = Self::extract_ical_property(&ical_data, "UID")
.unwrap_or_else(|| Uuid::new_v4().to_string()); .unwrap_or_else(|| Uuid::new_v4().to_string());
let now = Utc::now(); let now = Utc::now();
Ok(Self { Ok(Self {
id: Uuid::new_v4(), id: Uuid::new_v4(),
calendar_id, calendar_id,
@@ -293,84 +298,84 @@ impl CalendarEvent {
updated_at: now, updated_at: now,
}) })
} }
// Getters // Getters
/// Returns the event's unique identifier /// Returns the event's unique identifier
pub fn id(&self) -> &Uuid { pub fn id(&self) -> &Uuid {
&self.id &self.id
} }
/// Returns the ID of the calendar this event belongs to /// Returns the ID of the calendar this event belongs to
pub fn calendar_id(&self) -> &Uuid { pub fn calendar_id(&self) -> &Uuid {
&self.calendar_id &self.calendar_id
} }
/// Returns the event's summary/title /// Returns the event's summary/title
pub fn summary(&self) -> &str { pub fn summary(&self) -> &str {
&self.summary &self.summary
} }
/// Returns the event's description, if any /// Returns the event's description, if any
pub fn description(&self) -> Option<&str> { pub fn description(&self) -> Option<&str> {
self.description.as_deref() self.description.as_deref()
} }
/// Returns the event's location, if any /// Returns the event's location, if any
pub fn location(&self) -> Option<&str> { pub fn location(&self) -> Option<&str> {
self.location.as_deref() self.location.as_deref()
} }
/// Returns the event's start time /// Returns the event's start time
pub fn start_time(&self) -> &DateTime<Utc> { pub fn start_time(&self) -> &DateTime<Utc> {
&self.start_time &self.start_time
} }
/// Returns the event's end time /// Returns the event's end time
pub fn end_time(&self) -> &DateTime<Utc> { pub fn end_time(&self) -> &DateTime<Utc> {
&self.end_time &self.end_time
} }
/// Returns whether this is an all-day event /// Returns whether this is an all-day event
pub fn all_day(&self) -> bool { pub fn all_day(&self) -> bool {
self.all_day self.all_day
} }
/// Returns the event's recurrence rule, if any /// Returns the event's recurrence rule, if any
pub fn rrule(&self) -> Option<&str> { pub fn rrule(&self) -> Option<&str> {
self.rrule.as_deref() self.rrule.as_deref()
} }
/// Returns the event's iCalendar UID /// Returns the event's iCalendar UID
pub fn ical_uid(&self) -> &str { pub fn ical_uid(&self) -> &str {
&self.ical_uid &self.ical_uid
} }
/// Returns the complete iCalendar data for the event /// Returns the complete iCalendar data for the event
pub fn ical_data(&self) -> &str { pub fn ical_data(&self) -> &str {
&self.ical_data &self.ical_data
} }
/// Returns the time when the event was created /// Returns the time when the event was created
pub fn created_at(&self) -> &DateTime<Utc> { pub fn created_at(&self) -> &DateTime<Utc> {
&self.created_at &self.created_at
} }
/// Returns the time when the event was last modified /// Returns the time when the event was last modified
pub fn updated_at(&self) -> &DateTime<Utc> { pub fn updated_at(&self) -> &DateTime<Utc> {
&self.updated_at &self.updated_at
} }
/// Returns the duration of the event /// Returns the duration of the event
pub fn duration(&self) -> Duration { pub fn duration(&self) -> Duration {
self.end_time - self.start_time self.end_time - self.start_time
} }
// Setters and Mutators // Setters and Mutators
/** /**
* Updates the event's summary/title. * Updates the event's summary/title.
* *
* @param summary New summary/title for the event * @param summary New summary/title for the event
* @return Result indicating success or containing a domain error * @return Result indicating success or containing a domain error
*/ */
@@ -382,58 +387,62 @@ impl CalendarEvent {
"Event summary cannot be empty", "Event summary cannot be empty",
)); ));
} }
// Clone the summary before updating the struct // Clone the summary before updating the struct
let summary_clone = summary.clone(); let summary_clone = summary.clone();
self.summary = summary; self.summary = summary;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
// Update iCalendar data using the cloned value // Update iCalendar data using the cloned value
self.update_ical_property("SUMMARY", &summary_clone); self.update_ical_property("SUMMARY", &summary_clone);
Ok(()) Ok(())
} }
/** /**
* Updates the event's description. * Updates the event's description.
* *
* @param description New description for the event * @param description New description for the event
*/ */
pub fn update_description(&mut self, description: Option<String>) { pub fn update_description(&mut self, description: Option<String>) {
self.description = description.clone(); self.description = description.clone();
self.updated_at = Utc::now(); self.updated_at = Utc::now();
// Update iCalendar data // Update iCalendar data
match description { match description {
Some(desc) => self.update_ical_property("DESCRIPTION", &desc), Some(desc) => self.update_ical_property("DESCRIPTION", &desc),
None => self.remove_ical_property("DESCRIPTION"), None => self.remove_ical_property("DESCRIPTION"),
} }
} }
/** /**
* Updates the event's location. * Updates the event's location.
* *
* @param location New location for the event * @param location New location for the event
*/ */
pub fn update_location(&mut self, location: Option<String>) { pub fn update_location(&mut self, location: Option<String>) {
self.location = location.clone(); self.location = location.clone();
self.updated_at = Utc::now(); self.updated_at = Utc::now();
// Update iCalendar data // Update iCalendar data
match location { match location {
Some(loc) => self.update_ical_property("LOCATION", &loc), Some(loc) => self.update_ical_property("LOCATION", &loc),
None => self.remove_ical_property("LOCATION"), None => self.remove_ical_property("LOCATION"),
} }
} }
/** /**
* Updates the event's start and end times. * Updates the event's start and end times.
* *
* @param start_time New start time for the event * @param start_time New start time for the event
* @param end_time New end time for the event * @param end_time New end time for the event
* @return Result indicating success or containing a domain error * @return Result indicating success or containing a domain error
*/ */
pub fn update_time_range(&mut self, start_time: DateTime<Utc>, end_time: DateTime<Utc>) -> Result<()> { pub fn update_time_range(
&mut self,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
) -> Result<()> {
if end_time < start_time { if end_time < start_time {
return Err(DomainError::new( return Err(DomainError::new(
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
@@ -441,89 +450,90 @@ impl CalendarEvent {
"End time cannot be before start time", "End time cannot be before start time",
)); ));
} }
self.start_time = start_time; self.start_time = start_time;
self.end_time = end_time; self.end_time = end_time;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
// Update iCalendar data // Update iCalendar data
let start_str = if self.all_day { let start_str = if self.all_day {
format!("{}T000000Z", start_time.format("%Y%m%d")) format!("{}T000000Z", start_time.format("%Y%m%d"))
} else { } else {
format!("{}", start_time.format("%Y%m%dT%H%M%SZ")) format!("{}", start_time.format("%Y%m%dT%H%M%SZ"))
}; };
let end_str = if self.all_day { let end_str = if self.all_day {
format!("{}T000000Z", end_time.format("%Y%m%d")) format!("{}T000000Z", end_time.format("%Y%m%d"))
} else { } else {
format!("{}", end_time.format("%Y%m%dT%H%M%SZ")) format!("{}", end_time.format("%Y%m%dT%H%M%SZ"))
}; };
self.update_ical_property("DTSTART", &start_str); self.update_ical_property("DTSTART", &start_str);
self.update_ical_property("DTEND", &end_str); self.update_ical_property("DTEND", &end_str);
Ok(()) Ok(())
} }
/** /**
* Updates whether this is an all-day event. * Updates whether this is an all-day event.
* *
* @param all_day Whether this is an all-day event * @param all_day Whether this is an all-day event
*/ */
pub fn update_all_day(&mut self, all_day: bool) { pub fn update_all_day(&mut self, all_day: bool) {
self.all_day = all_day; self.all_day = all_day;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
// Update iCalendar data // Update iCalendar data
let start_str = if all_day { let start_str = if all_day {
format!("VALUE=DATE:{}", self.start_time.format("%Y%m%d")) format!("VALUE=DATE:{}", self.start_time.format("%Y%m%d"))
} else { } else {
format!("{}", self.start_time.format("%Y%m%dT%H%M%SZ")) format!("{}", self.start_time.format("%Y%m%dT%H%M%SZ"))
}; };
let end_str = if all_day { let end_str = if all_day {
format!("VALUE=DATE:{}", self.end_time.format("%Y%m%d")) format!("VALUE=DATE:{}", self.end_time.format("%Y%m%d"))
} else { } else {
format!("{}", self.end_time.format("%Y%m%dT%H%M%SZ")) format!("{}", self.end_time.format("%Y%m%dT%H%M%SZ"))
}; };
self.update_ical_property("DTSTART", &start_str); self.update_ical_property("DTSTART", &start_str);
self.update_ical_property("DTEND", &end_str); self.update_ical_property("DTEND", &end_str);
} }
/** /**
* Updates the event's recurrence rule. * Updates the event's recurrence rule.
* *
* @param rrule New recurrence rule for the event * @param rrule New recurrence rule for the event
* @return Result indicating success or containing a domain error * @return Result indicating success or containing a domain error
*/ */
pub fn update_rrule(&mut self, rrule: Option<String>) -> Result<()> { pub fn update_rrule(&mut self, rrule: Option<String>) -> Result<()> {
// Validate RRULE if provided (basic validation) // Validate RRULE if provided (basic validation)
if let Some(ref rule) = rrule if let Some(ref rule) = rrule
&& !rule.starts_with("FREQ=") { && !rule.starts_with("FREQ=")
return Err(DomainError::new( {
ErrorKind::InvalidInput, return Err(DomainError::new(
"CalendarEvent", ErrorKind::InvalidInput,
"Recurrence rule must start with FREQ=", "CalendarEvent",
)); "Recurrence rule must start with FREQ=",
} ));
}
self.rrule = rrule.clone(); self.rrule = rrule.clone();
self.updated_at = Utc::now(); self.updated_at = Utc::now();
// Update iCalendar data // Update iCalendar data
match rrule { match rrule {
Some(rule) => self.update_ical_property("RRULE", &rule), Some(rule) => self.update_ical_property("RRULE", &rule),
None => self.remove_ical_property("RRULE"), None => self.remove_ical_property("RRULE"),
} }
Ok(()) Ok(())
} }
/** /**
* Updates the complete iCalendar data for the event. * Updates the complete iCalendar data for the event.
* Also updates the event properties based on the new iCalendar data. * Also updates the event properties based on the new iCalendar data.
* *
* @param ical_data New iCalendar data for the event * @param ical_data New iCalendar data for the event
* @return Result indicating success or containing a domain error * @return Result indicating success or containing a domain error
*/ */
@@ -536,55 +546,57 @@ impl CalendarEvent {
"iCalendar data must contain a VEVENT component", "iCalendar data must contain a VEVENT component",
)); ));
} }
// Extract and update properties from iCalendar data // Extract and update properties from iCalendar data
if let Some(summary) = Self::extract_ical_property(&ical_data, "SUMMARY") { if let Some(summary) = Self::extract_ical_property(&ical_data, "SUMMARY") {
self.summary = summary; self.summary = summary;
} }
self.description = Self::extract_ical_property(&ical_data, "DESCRIPTION"); self.description = Self::extract_ical_property(&ical_data, "DESCRIPTION");
self.location = Self::extract_ical_property(&ical_data, "LOCATION"); self.location = Self::extract_ical_property(&ical_data, "LOCATION");
if let Some(dtstart) = Self::extract_ical_property(&ical_data, "DTSTART") if let Some(dtstart) = Self::extract_ical_property(&ical_data, "DTSTART")
&& let Ok(start_time) = Self::parse_ical_datetime(&dtstart) { && let Ok(start_time) = Self::parse_ical_datetime(&dtstart)
self.start_time = start_time; {
} self.start_time = start_time;
}
if let Some(dtend) = Self::extract_ical_property(&ical_data, "DTEND") if let Some(dtend) = Self::extract_ical_property(&ical_data, "DTEND")
&& let Ok(end_time) = Self::parse_ical_datetime(&dtend) { && let Ok(end_time) = Self::parse_ical_datetime(&dtend)
self.end_time = end_time; {
} self.end_time = end_time;
}
// Update all-day status based on DTSTART // Update all-day status based on DTSTART
if let Some(dtstart) = Self::extract_ical_property(&ical_data, "DTSTART") { if let Some(dtstart) = Self::extract_ical_property(&ical_data, "DTSTART") {
self.all_day = dtstart.contains("VALUE=DATE") && !dtstart.contains("T"); self.all_day = dtstart.contains("VALUE=DATE") && !dtstart.contains("T");
} }
self.rrule = Self::extract_ical_property(&ical_data, "RRULE"); self.rrule = Self::extract_ical_property(&ical_data, "RRULE");
if let Some(uid) = Self::extract_ical_property(&ical_data, "UID") { if let Some(uid) = Self::extract_ical_property(&ical_data, "UID") {
self.ical_uid = uid; self.ical_uid = uid;
} }
self.ical_data = ical_data; self.ical_data = ical_data;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
Ok(()) Ok(())
} }
/** /**
* Checks if this event belongs to the specified calendar. * Checks if this event belongs to the specified calendar.
* *
* @param calendar_id ID of the calendar to check against * @param calendar_id ID of the calendar to check against
* @return true if the event belongs to the calendar, false otherwise * @return true if the event belongs to the calendar, false otherwise
*/ */
pub fn belongs_to_calendar(&self, calendar_id: &Uuid) -> bool { pub fn belongs_to_calendar(&self, calendar_id: &Uuid) -> bool {
self.calendar_id == *calendar_id self.calendar_id == *calendar_id
} }
/** /**
* Checks if this event occurs within the specified time range. * Checks if this event occurs within the specified time range.
* *
* @param start Start of the time range to check * @param start Start of the time range to check
* @param end End of the time range to check * @param end End of the time range to check
* @return true if the event occurs within the range, false otherwise * @return true if the event occurs within the range, false otherwise
@@ -594,20 +606,20 @@ impl CalendarEvent {
if self.start_time <= *end && self.end_time >= *start { if self.start_time <= *end && self.end_time >= *start {
return true; return true;
} }
// If event has recurrence, check if any recurrence occurs in range // If event has recurrence, check if any recurrence occurs in range
// Note: A full implementation would need a proper recurrence rule parser // Note: A full implementation would need a proper recurrence rule parser
if let Some(rrule) = &self.rrule { if let Some(rrule) = &self.rrule {
// Simplified check for demonstration // Simplified check for demonstration
// A real implementation would need to generate recurrence instances // A real implementation would need to generate recurrence instances
// and check if any fall within the range // and check if any fall within the range
// For now, we'll just check if the recurrence hasn't ended // For now, we'll just check if the recurrence hasn't ended
// or if it ended after the start of our range // or if it ended after the start of our range
if let Some(until_pos) = rrule.find("UNTIL=") { if let Some(until_pos) = rrule.find("UNTIL=") {
let until_start = until_pos + 6; // "UNTIL=" is 6 chars let until_start = until_pos + 6; // "UNTIL=" is 6 chars
if let Some(until_end) = rrule[until_start..].find(';') { if let Some(until_end) = rrule[until_start..].find(';') {
let until_str = &rrule[until_start..until_start+until_end]; let until_str = &rrule[until_start..until_start + until_end];
if let Ok(until_date) = Self::parse_ical_datetime(until_str) { if let Ok(until_date) = Self::parse_ical_datetime(until_str) {
return until_date >= *start; return until_date >= *start;
} }
@@ -623,15 +635,15 @@ impl CalendarEvent {
return true; return true;
} }
} }
false false
} }
// Helper methods for iCalendar operations // Helper methods for iCalendar operations
/** /**
* Extracts a property value from iCalendar data. * Extracts a property value from iCalendar data.
* *
* @param ical_data The iCalendar data to search in * @param ical_data The iCalendar data to search in
* @param property_name The name of the property to extract * @param property_name The name of the property to extract
* @return Option containing the property value if found * @return Option containing the property value if found
@@ -640,33 +652,34 @@ impl CalendarEvent {
// Find the property in the iCalendar data // Find the property in the iCalendar data
let search_str = format!("\n{}:", property_name); let search_str = format!("\n{}:", property_name);
let search_str_alt = format!("\r\n{}:", property_name); let search_str_alt = format!("\r\n{}:", property_name);
let pos = ical_data.find(&search_str) let pos = ical_data
.find(&search_str)
.or_else(|| ical_data.find(&search_str_alt)); .or_else(|| ical_data.find(&search_str_alt));
if let Some(pos) = pos { if let Some(pos) = pos {
// Find the start of the value // Find the start of the value
let value_start = pos + search_str.len(); let value_start = pos + search_str.len();
// Find the end of the value (next line or end of string) // Find the end of the value (next line or end of string)
let value_end = ical_data[value_start..] let value_end = ical_data[value_start..]
.find('\n') .find('\n')
.map(|p| value_start + p) .map(|p| value_start + p)
.unwrap_or_else(|| ical_data.len()); .unwrap_or_else(|| ical_data.len());
// Extract and return the value // Extract and return the value
let value = ical_data[value_start..value_end].trim(); let value = ical_data[value_start..value_end].trim();
if !value.is_empty() { if !value.is_empty() {
return Some(value.to_string()); return Some(value.to_string());
} }
} }
None None
} }
/** /**
* Parses an iCalendar datetime string into a DateTime object. * Parses an iCalendar datetime string into a DateTime object.
* *
* @param datetime The iCalendar datetime string to parse * @param datetime The iCalendar datetime string to parse
* @return Result containing the parsed DateTime or an error * @return Result containing the parsed DateTime or an error
*/ */
@@ -677,40 +690,49 @@ impl CalendarEvent {
if date_str.len() != 8 { if date_str.len() != 8 {
return Err("Invalid date format".to_string()); return Err("Invalid date format".to_string());
} }
let year = date_str[0..4].parse::<i32>() let year = date_str[0..4]
.parse::<i32>()
.map_err(|_| "Invalid year".to_string())?; .map_err(|_| "Invalid year".to_string())?;
let month = date_str[4..6].parse::<u32>() let month = date_str[4..6]
.parse::<u32>()
.map_err(|_| "Invalid month".to_string())?; .map_err(|_| "Invalid month".to_string())?;
let day = date_str[6..8].parse::<u32>() let day = date_str[6..8]
.parse::<u32>()
.map_err(|_| "Invalid day".to_string())?; .map_err(|_| "Invalid day".to_string())?;
return match chrono::NaiveDate::from_ymd_opt(year, month, day) { return match chrono::NaiveDate::from_ymd_opt(year, month, day) {
Some(date) => Ok(Utc.from_utc_datetime(&date.and_hms_opt(0, 0, 0).unwrap())), Some(date) => Ok(Utc.from_utc_datetime(&date.and_hms_opt(0, 0, 0).unwrap())),
None => Err("Invalid date components".to_string()), None => Err("Invalid date components".to_string()),
}; };
} }
// Handle standard UTC format (20230101T120000Z) // Handle standard UTC format (20230101T120000Z)
let datetime_str = datetime.split(':').next_back().unwrap_or(datetime); let datetime_str = datetime.split(':').next_back().unwrap_or(datetime);
if datetime_str.len() < 15 || !datetime_str.ends_with('Z') { if datetime_str.len() < 15 || !datetime_str.ends_with('Z') {
return Err("Invalid datetime format".to_string()); return Err("Invalid datetime format".to_string());
} }
let year = datetime_str[0..4].parse::<i32>() let year = datetime_str[0..4]
.parse::<i32>()
.map_err(|_| "Invalid year".to_string())?; .map_err(|_| "Invalid year".to_string())?;
let month = datetime_str[4..6].parse::<u32>() let month = datetime_str[4..6]
.parse::<u32>()
.map_err(|_| "Invalid month".to_string())?; .map_err(|_| "Invalid month".to_string())?;
let day = datetime_str[6..8].parse::<u32>() let day = datetime_str[6..8]
.parse::<u32>()
.map_err(|_| "Invalid day".to_string())?; .map_err(|_| "Invalid day".to_string())?;
let hour = datetime_str[9..11].parse::<u32>() let hour = datetime_str[9..11]
.parse::<u32>()
.map_err(|_| "Invalid hour".to_string())?; .map_err(|_| "Invalid hour".to_string())?;
let minute = datetime_str[11..13].parse::<u32>() let minute = datetime_str[11..13]
.parse::<u32>()
.map_err(|_| "Invalid minute".to_string())?; .map_err(|_| "Invalid minute".to_string())?;
let second = datetime_str[13..15].parse::<u32>() let second = datetime_str[13..15]
.parse::<u32>()
.map_err(|_| "Invalid second".to_string())?; .map_err(|_| "Invalid second".to_string())?;
match chrono::NaiveDate::from_ymd_opt(year, month, day) { match chrono::NaiveDate::from_ymd_opt(year, month, day) {
Some(date) => match date.and_hms_opt(hour, minute, second) { Some(date) => match date.and_hms_opt(hour, minute, second) {
Some(datetime) => Ok(Utc.from_utc_datetime(&datetime)), Some(datetime) => Ok(Utc.from_utc_datetime(&datetime)),
@@ -719,70 +741,76 @@ impl CalendarEvent {
None => Err("Invalid date components".to_string()), None => Err("Invalid date components".to_string()),
} }
} }
/** /**
* Updates an iCalendar property in the event's iCalendar data. * Updates an iCalendar property in the event's iCalendar data.
* *
* @param property_name The name of the property to update * @param property_name The name of the property to update
* @param value The new value for the property * @param value The new value for the property
*/ */
fn update_ical_property(&mut self, property_name: &str, value: &str) { fn update_ical_property(&mut self, property_name: &str, value: &str) {
let search_str = format!("\n{}:", property_name); let search_str = format!("\n{}:", property_name);
let search_str_alt = format!("\r\n{}:", property_name); let search_str_alt = format!("\r\n{}:", property_name);
// Check if property exists // Check if property exists
let pos = self.ical_data.find(&search_str) let pos = self
.ical_data
.find(&search_str)
.or_else(|| self.ical_data.find(&search_str_alt)); .or_else(|| self.ical_data.find(&search_str_alt));
if let Some(pos) = pos { if let Some(pos) = pos {
// Find the start of the value // Find the start of the value
let value_start = pos + search_str.len(); let value_start = pos + search_str.len();
// Find the end of the value (next line or end of string) // Find the end of the value (next line or end of string)
let value_end = self.ical_data[value_start..] let value_end = self.ical_data[value_start..]
.find('\n') .find('\n')
.map(|p| value_start + p) .map(|p| value_start + p)
.unwrap_or_else(|| self.ical_data.len()); .unwrap_or_else(|| self.ical_data.len());
// Replace the value // Replace the value
let before = &self.ical_data[..value_start]; let before = &self.ical_data[..value_start];
let after = &self.ical_data[value_end..]; let after = &self.ical_data[value_end..];
self.ical_data = format!("{}{}{}", before, value, after); self.ical_data = format!("{}{}{}", before, value, after);
} else { } else {
// Property doesn't exist, add it before END:VEVENT // Property doesn't exist, add it before END:VEVENT
let end_pos = self.ical_data.find("END:VEVENT") let end_pos = self
.ical_data
.find("END:VEVENT")
.unwrap_or(self.ical_data.len()); .unwrap_or(self.ical_data.len());
let before = &self.ical_data[..end_pos]; let before = &self.ical_data[..end_pos];
let after = &self.ical_data[end_pos..]; let after = &self.ical_data[end_pos..];
self.ical_data = format!("{}{}:{}\n{}", before, property_name, value, after); self.ical_data = format!("{}{}:{}\n{}", before, property_name, value, after);
} }
} }
/** /**
* Removes an iCalendar property from the event's iCalendar data. * Removes an iCalendar property from the event's iCalendar data.
* *
* @param property_name The name of the property to remove * @param property_name The name of the property to remove
*/ */
fn remove_ical_property(&mut self, property_name: &str) { fn remove_ical_property(&mut self, property_name: &str) {
let search_str = format!("\n{}:", property_name); let search_str = format!("\n{}:", property_name);
let search_str_alt = format!("\r\n{}:", property_name); let search_str_alt = format!("\r\n{}:", property_name);
// Check if property exists // Check if property exists
let pos = self.ical_data.find(&search_str) let pos = self
.ical_data
.find(&search_str)
.or_else(|| self.ical_data.find(&search_str_alt)); .or_else(|| self.ical_data.find(&search_str_alt));
if let Some(pos) = pos { if let Some(pos) = pos {
// Find the end of the value (next line or end of string) // Find the end of the value (next line or end of string)
let value_end = self.ical_data[pos + 1..] let value_end = self.ical_data[pos + 1..]
.find('\n') .find('\n')
.map(|p| pos + 1 + p) .map(|p| pos + 1 + p)
.unwrap_or_else(|| self.ical_data.len()); .unwrap_or_else(|| self.ical_data.len());
// Remove the property // Remove the property
let before = &self.ical_data[..pos]; let before = &self.ical_data[..pos];
let after = &self.ical_data[value_end..]; let after = &self.ical_data[value_end..];
self.ical_data = format!("{}{}", before, after); self.ical_data = format!("{}{}", before, after);
} }
} }
} }
+263 -78
View File
@@ -46,25 +46,64 @@ impl AddressBook {
created_at: DateTime<Utc>, created_at: DateTime<Utc>,
updated_at: DateTime<Utc>, updated_at: DateTime<Utc>,
) -> Self { ) -> Self {
Self { id, name, owner_id, description, color, is_public, created_at, updated_at } Self {
id,
name,
owner_id,
description,
color,
is_public,
created_at,
updated_at,
}
} }
// --- Getters --- // --- Getters ---
pub fn id(&self) -> &Uuid { &self.id } pub fn id(&self) -> &Uuid {
pub fn name(&self) -> &str { &self.name } &self.id
pub fn owner_id(&self) -> &str { &self.owner_id } }
pub fn description(&self) -> Option<&str> { self.description.as_deref() } pub fn name(&self) -> &str {
pub fn color(&self) -> Option<&str> { self.color.as_deref() } &self.name
pub fn is_public(&self) -> bool { self.is_public } }
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at } pub fn owner_id(&self) -> &str {
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at } &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 --- // --- Setters for mutable operations ---
pub fn set_name(&mut self, name: String) { self.name = name; self.updated_at = Utc::now(); } pub fn set_name(&mut self, name: String) {
pub fn set_description(&mut self, description: Option<String>) { self.description = description; self.updated_at = Utc::now(); } self.name = name;
pub fn set_color(&mut self, color: Option<String>) { self.color = color; self.updated_at = Utc::now(); } 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; } 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 { impl Default for AddressBook {
@@ -198,72 +237,191 @@ impl Contact {
updated_at: DateTime<Utc>, updated_at: DateTime<Utc>,
) -> Self { ) -> Self {
Self { Self {
id, address_book_id, uid, full_name, first_name, last_name, nickname, id,
email, phone, address, organization, title, notes, photo_url, address_book_id,
birthday, anniversary, vcard, etag, created_at, updated_at, 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 --- // --- Getters ---
pub fn id(&self) -> &Uuid { &self.id } pub fn id(&self) -> &Uuid {
pub fn address_book_id(&self) -> &Uuid { &self.address_book_id } &self.id
pub fn uid(&self) -> &str { &self.uid } }
pub fn full_name(&self) -> Option<&str> { self.full_name.as_deref() } pub fn address_book_id(&self) -> &Uuid {
pub fn first_name(&self) -> Option<&str> { self.first_name.as_deref() } &self.address_book_id
pub fn last_name(&self) -> Option<&str> { self.last_name.as_deref() } }
pub fn nickname(&self) -> Option<&str> { self.nickname.as_deref() } pub fn uid(&self) -> &str {
pub fn email(&self) -> &[Email] { &self.email } &self.uid
pub fn phone(&self) -> &[Phone] { &self.phone } }
pub fn address(&self) -> &[Address] { &self.address } pub fn full_name(&self) -> Option<&str> {
pub fn organization(&self) -> Option<&str> { self.organization.as_deref() } self.full_name.as_deref()
pub fn title(&self) -> Option<&str> { self.title.as_deref() } }
pub fn notes(&self) -> Option<&str> { self.notes.as_deref() } pub fn first_name(&self) -> Option<&str> {
pub fn photo_url(&self) -> Option<&str> { self.photo_url.as_deref() } self.first_name.as_deref()
pub fn birthday(&self) -> Option<&NaiveDate> { self.birthday.as_ref() } }
pub fn anniversary(&self) -> Option<&NaiveDate> { self.anniversary.as_ref() } pub fn last_name(&self) -> Option<&str> {
pub fn vcard(&self) -> &str { &self.vcard } self.last_name.as_deref()
pub fn etag(&self) -> &str { &self.etag } }
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at } pub fn nickname(&self) -> Option<&str> {
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at } 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 --- // --- Owned getters for persistence layer bind() calls ---
pub fn full_name_owned(&self) -> Option<String> { self.full_name.clone() } pub fn full_name_owned(&self) -> Option<String> {
pub fn first_name_owned(&self) -> Option<String> { self.first_name.clone() } self.full_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 first_name_owned(&self) -> Option<String> {
pub fn organization_owned(&self) -> Option<String> { self.organization.clone() } self.first_name.clone()
pub fn title_owned(&self) -> Option<String> { self.title.clone() } }
pub fn notes_owned(&self) -> Option<String> { self.notes.clone() } pub fn last_name_owned(&self) -> Option<String> {
pub fn photo_url_owned(&self) -> Option<String> { self.photo_url.clone() } 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) --- // --- 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_full_name(&mut self, v: Option<String>) {
pub fn set_first_name(&mut self, v: Option<String>) { self.first_name = v; } self.full_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_first_name(&mut self, v: Option<String>) {
pub fn set_organization(&mut self, v: Option<String>) { self.organization = v; } self.first_name = 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_last_name(&mut self, v: Option<String>) {
pub fn set_photo_url(&mut self, v: Option<String>) { self.photo_url = v; } self.last_name = 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_nickname(&mut self, v: Option<String>) {
pub fn set_vcard(&mut self, vcard: String) { self.vcard = vcard; } self.nickname = v;
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_organization(&mut self, v: Option<String>) {
pub fn set_address_book_id(&mut self, id: Uuid) { self.address_book_id = id; } self.organization = v;
pub fn set_uid(&mut self, uid: String) { self.uid = uid; } }
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 --- // --- Collection mutators ---
pub fn push_email(&mut self, e: Email) { self.email.push(e); } pub fn push_email(&mut self, e: Email) {
pub fn push_phone(&mut self, p: Phone) { self.phone.push(p); } self.email.push(e);
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 push_phone(&mut self, p: Phone) {
pub fn set_address(&mut self, address: Vec<Address>) { self.address = address; } self.phone.push(p);
pub fn email_is_empty(&self) -> bool { self.email.is_empty() } }
pub fn phone_is_empty(&self) -> bool { self.phone.is_empty() } 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 --- // --- Consuming methods for ownership transfer ---
pub fn into_email(self) -> Vec<Email> { self.email } pub fn into_email(self) -> Vec<Email> {
self.email
}
pub fn into_parts(self) -> ContactParts { pub fn into_parts(self) -> ContactParts {
ContactParts { ContactParts {
id: self.id, id: self.id,
@@ -355,7 +513,13 @@ impl ContactGroup {
/// Creates a new ContactGroup with generated id and timestamps /// Creates a new ContactGroup with generated id and timestamps
pub fn new(address_book_id: Uuid, name: String) -> Self { pub fn new(address_book_id: Uuid, name: String) -> Self {
let now = Utc::now(); let now = Utc::now();
Self { id: Uuid::new_v4(), address_book_id, name, created_at: now, updated_at: now } Self {
id: Uuid::new_v4(),
address_book_id,
name,
created_at: now,
updated_at: now,
}
} }
/// Reconstructs from persistence /// Reconstructs from persistence
@@ -366,23 +530,44 @@ impl ContactGroup {
created_at: DateTime<Utc>, created_at: DateTime<Utc>,
updated_at: DateTime<Utc>, updated_at: DateTime<Utc>,
) -> Self { ) -> Self {
Self { id, address_book_id, name, created_at, updated_at } Self {
id,
address_book_id,
name,
created_at,
updated_at,
}
} }
// --- Getters --- // --- Getters ---
pub fn id(&self) -> &Uuid { &self.id } pub fn id(&self) -> &Uuid {
pub fn address_book_id(&self) -> &Uuid { &self.address_book_id } &self.id
pub fn name(&self) -> &str { &self.name } }
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at } pub fn address_book_id(&self) -> &Uuid {
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at } &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 --- // --- Setters ---
pub fn set_name(&mut self, name: String) { self.name = name; self.updated_at = Utc::now(); } pub fn set_name(&mut self, name: String) {
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; } 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 { impl Default for ContactGroup {
fn default() -> Self { fn default() -> Self {
ContactGroup::new(Uuid::new_v4(), "New Group".to_string()) ContactGroup::new(Uuid::new_v4(), "New Group".to_string())
} }
} }
+258 -254
View File
@@ -1,254 +1,258 @@
//! Pure domain entity errors //! Pure domain entity errors
//! //!
//! This module defines domain entity-specific errors //! This module defines domain entity-specific errors
//! without external framework dependencies, following //! without external framework dependencies, following
//! Clean Architecture principles. //! Clean Architecture principles.
//! //!
//! Errors manually implement `std::error::Error` and `std::fmt::Display` //! Errors manually implement `std::error::Error` and `std::fmt::Display`
//! to keep the domain free of external dependencies. //! to keep the domain free of external dependencies.
use std::error::Error; use std::error::Error;
use std::fmt::{Display, Formatter, Result as FmtResult}; use std::fmt::{Display, Formatter, Result as FmtResult};
// ============================================================================ // ============================================================================
// FILE ERRORS // FILE ERRORS
// ============================================================================ // ============================================================================
/// Errors that can occur during File entity operations /// Errors that can occur during File entity operations
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileError { pub enum FileError {
/// Occurs when the file name contains invalid characters or is empty /// Occurs when the file name contains invalid characters or is empty
InvalidFileName(String), InvalidFileName(String),
/// Occurs when validation of any entity attribute fails /// Occurs when validation of any entity attribute fails
ValidationError(String), ValidationError(String),
} }
impl Display for FileError { impl Display for FileError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self { match self {
FileError::InvalidFileName(name) => write!(f, "Invalid file name: {}", name), FileError::InvalidFileName(name) => write!(f, "Invalid file name: {}", name),
FileError::ValidationError(msg) => write!(f, "Validation error: {}", msg), FileError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
} }
} }
} }
impl Error for FileError {} impl Error for FileError {}
/// Type alias for File entity operation results /// Type alias for File entity operation results
pub type FileResult<T> = Result<T, FileError>; pub type FileResult<T> = Result<T, FileError>;
// ============================================================================ // ============================================================================
// FOLDER ERRORS // FOLDER ERRORS
// ============================================================================ // ============================================================================
/// Errors that can occur during Folder entity operations /// Errors that can occur during Folder entity operations
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum FolderError { pub enum FolderError {
/// Occurs when the folder name contains invalid characters or is empty /// Occurs when the folder name contains invalid characters or is empty
InvalidFolderName(String), InvalidFolderName(String),
/// Occurs when validation of any entity attribute fails /// Occurs when validation of any entity attribute fails
ValidationError(String), ValidationError(String),
} }
impl Display for FolderError { impl Display for FolderError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self { match self {
FolderError::InvalidFolderName(name) => write!(f, "Invalid folder name: {}", name), FolderError::InvalidFolderName(name) => write!(f, "Invalid folder name: {}", name),
FolderError::ValidationError(msg) => write!(f, "Validation error: {}", msg), FolderError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
} }
} }
} }
impl Error for FolderError {} impl Error for FolderError {}
/// Type alias for Folder entity operation results /// Type alias for Folder entity operation results
pub type FolderResult<T> = Result<T, FolderError>; pub type FolderResult<T> = Result<T, FolderError>;
// ============================================================================ // ============================================================================
// USER ERRORS // USER ERRORS
// ============================================================================ // ============================================================================
/// Errors that can occur during User entity operations /// Errors that can occur during User entity operations
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum UserError { pub enum UserError {
/// Invalid username /// Invalid username
InvalidUsername(String), InvalidUsername(String),
/// Invalid password /// Invalid password
InvalidPassword(String), InvalidPassword(String),
/// General validation error /// General validation error
ValidationError(String), ValidationError(String),
/// Authentication error /// Authentication error
AuthenticationError(String), AuthenticationError(String),
} }
impl Display for UserError { impl Display for UserError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self { match self {
UserError::InvalidUsername(msg) => write!(f, "Invalid username: {}", msg), UserError::InvalidUsername(msg) => write!(f, "Invalid username: {}", msg),
UserError::InvalidPassword(msg) => write!(f, "Invalid password: {}", msg), UserError::InvalidPassword(msg) => write!(f, "Invalid password: {}", msg),
UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg), UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
UserError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg), UserError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg),
} }
} }
} }
impl Error for UserError {} impl Error for UserError {}
/// Type alias for User entity operation results /// Type alias for User entity operation results
pub type UserResult<T> = Result<T, UserError>; pub type UserResult<T> = Result<T, UserError>;
// ============================================================================ // ============================================================================
// SHARE ERRORS // SHARE ERRORS
// ============================================================================ // ============================================================================
/// Errors that can occur during Share entity operations /// Errors that can occur during Share entity operations
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShareError { pub enum ShareError {
/// Invalid share token /// Invalid share token
InvalidToken(String), InvalidToken(String),
/// Invalid expiration date /// Invalid expiration date
InvalidExpiration(String), InvalidExpiration(String),
/// General validation error /// General validation error
ValidationError(String), ValidationError(String),
} }
impl Display for ShareError { impl Display for ShareError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self { match self {
ShareError::InvalidToken(msg) => write!(f, "Invalid token: {}", msg), ShareError::InvalidToken(msg) => write!(f, "Invalid token: {}", msg),
ShareError::InvalidExpiration(msg) => write!(f, "Invalid expiration date: {}", msg), ShareError::InvalidExpiration(msg) => write!(f, "Invalid expiration date: {}", msg),
ShareError::ValidationError(msg) => write!(f, "Validation error: {}", msg), ShareError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
} }
} }
} }
impl Error for ShareError {} impl Error for ShareError {}
/// Type alias for Share entity operation results /// Type alias for Share entity operation results
pub type ShareResult<T> = Result<T, ShareError>; pub type ShareResult<T> = Result<T, ShareError>;
// ============================================================================ // ============================================================================
// CALENDAR ERRORS // CALENDAR ERRORS
// ============================================================================ // ============================================================================
/// Errors that can occur during Calendar entity operations /// Errors that can occur during Calendar entity operations
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarError { pub enum CalendarError {
/// Invalid calendar name /// Invalid calendar name
InvalidName(String), InvalidName(String),
/// Invalid color code /// Invalid color code
InvalidColor(String), InvalidColor(String),
/// Invalid owner ID /// Invalid owner ID
InvalidOwnerId(String), InvalidOwnerId(String),
} }
impl Display for CalendarError { impl Display for CalendarError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self { match self {
CalendarError::InvalidName(msg) => write!(f, "Invalid calendar name: {}", msg), CalendarError::InvalidName(msg) => write!(f, "Invalid calendar name: {}", msg),
CalendarError::InvalidColor(msg) => write!(f, "Invalid color code: {}", msg), CalendarError::InvalidColor(msg) => write!(f, "Invalid color code: {}", msg),
CalendarError::InvalidOwnerId(msg) => write!(f, "Invalid owner ID: {}", msg), CalendarError::InvalidOwnerId(msg) => write!(f, "Invalid owner ID: {}", msg),
} }
} }
} }
impl Error for CalendarError {} impl Error for CalendarError {}
/// Type alias for Calendar entity operation results /// Type alias for Calendar entity operation results
pub type CalendarResult<T> = Result<T, CalendarError>; pub type CalendarResult<T> = Result<T, CalendarError>;
// ============================================================================ // ============================================================================
// CALENDAR EVENT ERRORS // CALENDAR EVENT ERRORS
// ============================================================================ // ============================================================================
/// Errors that can occur during CalendarEvent entity operations /// Errors that can occur during CalendarEvent entity operations
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarEventError { pub enum CalendarEventError {
/// Invalid event summary/title /// Invalid event summary/title
InvalidSummary(String), InvalidSummary(String),
/// Invalid event dates /// Invalid event dates
InvalidDates(String), InvalidDates(String),
/// Invalid recurrence rule /// Invalid recurrence rule
InvalidRecurrence(String), InvalidRecurrence(String),
/// Invalid iCalendar data /// Invalid iCalendar data
InvalidICalData(String), InvalidICalData(String),
} }
impl Display for CalendarEventError { impl Display for CalendarEventError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self { match self {
CalendarEventError::InvalidSummary(msg) => write!(f, "Invalid event summary: {}", msg), CalendarEventError::InvalidSummary(msg) => write!(f, "Invalid event summary: {}", msg),
CalendarEventError::InvalidDates(msg) => write!(f, "Invalid event dates: {}", msg), CalendarEventError::InvalidDates(msg) => write!(f, "Invalid event dates: {}", msg),
CalendarEventError::InvalidRecurrence(msg) => write!(f, "Invalid recurrence rule: {}", msg), CalendarEventError::InvalidRecurrence(msg) => {
CalendarEventError::InvalidICalData(msg) => write!(f, "Invalid iCalendar data: {}", msg), write!(f, "Invalid recurrence rule: {}", msg)
} }
} CalendarEventError::InvalidICalData(msg) => {
} write!(f, "Invalid iCalendar data: {}", msg)
}
impl Error for CalendarEventError {} }
}
/// Type alias for CalendarEvent entity operation results }
pub type CalendarEventResult<T> = Result<T, CalendarEventError>;
impl Error for CalendarEventError {}
// ============================================================================
// TESTS /// Type alias for CalendarEvent entity operation results
// ============================================================================ pub type CalendarEventResult<T> = Result<T, CalendarEventError>;
#[cfg(test)] // ============================================================================
mod tests { // TESTS
use super::*; // ============================================================================
#[test] #[cfg(test)]
fn test_file_error_display() { mod tests {
let err = FileError::InvalidFileName("test.txt".to_string()); use super::*;
assert_eq!(err.to_string(), "Invalid file name: test.txt");
#[test]
let err = FileError::ValidationError("size too large".to_string()); fn test_file_error_display() {
assert_eq!(err.to_string(), "Validation error: size too large"); let err = FileError::InvalidFileName("test.txt".to_string());
} assert_eq!(err.to_string(), "Invalid file name: test.txt");
#[test] let err = FileError::ValidationError("size too large".to_string());
fn test_folder_error_display() { assert_eq!(err.to_string(), "Validation error: size too large");
let err = FolderError::InvalidFolderName("my/folder".to_string()); }
assert_eq!(err.to_string(), "Invalid folder name: my/folder");
} #[test]
fn test_folder_error_display() {
#[test] let err = FolderError::InvalidFolderName("my/folder".to_string());
fn test_user_error_display() { assert_eq!(err.to_string(), "Invalid folder name: my/folder");
let err = UserError::InvalidUsername("".to_string()); }
assert_eq!(err.to_string(), "Invalid username: ");
#[test]
let err = UserError::AuthenticationError("invalid credentials".to_string()); fn test_user_error_display() {
assert_eq!(err.to_string(), "Authentication error: invalid credentials"); let err = UserError::InvalidUsername("".to_string());
} assert_eq!(err.to_string(), "Invalid username: ");
#[test] let err = UserError::AuthenticationError("invalid credentials".to_string());
fn test_share_error_display() { assert_eq!(err.to_string(), "Authentication error: invalid credentials");
let err = ShareError::InvalidToken("abc123".to_string()); }
assert_eq!(err.to_string(), "Invalid token: abc123");
} #[test]
fn test_share_error_display() {
#[test] let err = ShareError::InvalidToken("abc123".to_string());
fn test_calendar_error_display() { assert_eq!(err.to_string(), "Invalid token: abc123");
let err = CalendarError::InvalidColor("not-a-color".to_string()); }
assert_eq!(err.to_string(), "Invalid color code: not-a-color");
} #[test]
fn test_calendar_error_display() {
#[test] let err = CalendarError::InvalidColor("not-a-color".to_string());
fn test_calendar_event_error_display() { assert_eq!(err.to_string(), "Invalid color code: not-a-color");
let err = CalendarEventError::InvalidDates("end before start".to_string()); }
assert_eq!(err.to_string(), "Invalid event dates: end before start");
} #[test]
fn test_calendar_event_error_display() {
#[test] let err = CalendarEventError::InvalidDates("end before start".to_string());
fn test_errors_implement_error_trait() { assert_eq!(err.to_string(), "Invalid event dates: end before start");
fn assert_error<E: Error>() {} }
assert_error::<FileError>(); #[test]
assert_error::<FolderError>(); fn test_errors_implement_error_trait() {
assert_error::<UserError>(); fn assert_error<E: Error>() {}
assert_error::<ShareError>();
assert_error::<CalendarError>(); assert_error::<FileError>();
assert_error::<CalendarEventError>(); assert_error::<FolderError>();
} assert_error::<UserError>();
} assert_error::<ShareError>();
assert_error::<CalendarError>();
assert_error::<CalendarEventError>();
}
}
+57 -52
View File
@@ -5,11 +5,11 @@ pub use super::entity_errors::{FileError, FileResult};
/** /**
* Represents a file in the system's domain model. * Represents a file in the system's domain model.
* *
* The File entity is a core domain object that encapsulates all properties and behaviors * The File entity is a core domain object that encapsulates all properties and behaviors
* of a file in the system. It implements an immutable design pattern where modification * of a file in the system. It implements an immutable design pattern where modification
* operations return new instances rather than modifying the existing one. * operations return new instances rather than modifying the existing one.
* *
* This entity maintains both physical storage information and logical metadata about files, * This entity maintains both physical storage information and logical metadata about files,
* serving as the bridge between the storage system and the application. * serving as the bridge between the storage system and the application.
*/ */
@@ -17,28 +17,28 @@ pub use super::entity_errors::{FileError, FileResult};
pub struct File { pub struct File {
/// Unique identifier for the file - used throughout the system for file operations /// Unique identifier for the file - used throughout the system for file operations
id: String, id: String,
/// Name of the file including extension /// Name of the file including extension
name: String, name: String,
/// Path to the file in the domain model /// Path to the file in the domain model
storage_path: StoragePath, storage_path: StoragePath,
/// String representation of the path for API compatibility /// String representation of the path for API compatibility
path_string: String, path_string: String,
/// Size of the file in bytes /// Size of the file in bytes
size: u64, size: u64,
/// MIME type of the file (e.g., "text/plain", "image/jpeg") /// MIME type of the file (e.g., "text/plain", "image/jpeg")
mime_type: String, mime_type: String,
/// Parent folder ID if the file is within a folder, None if in root /// Parent folder ID if the file is within a folder, None if in root
folder_id: Option<String>, folder_id: Option<String>,
/// Creation timestamp (seconds since UNIX epoch) /// Creation timestamp (seconds since UNIX epoch)
created_at: u64, created_at: u64,
/// Last modification timestamp (seconds since UNIX epoch) /// Last modification timestamp (seconds since UNIX epoch)
modified_at: u64, modified_at: u64,
} }
@@ -75,15 +75,15 @@ impl File {
if name.is_empty() || name.contains('/') || name.contains('\\') { if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name)); return Err(FileError::InvalidFileName(name));
} }
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
.as_secs(); .as_secs();
// Store the path string for serialization compatibility // Store the path string for serialization compatibility
let path_string = storage_path.to_string(); let path_string = storage_path.to_string();
Ok(Self { Ok(Self {
id, id,
name, name,
@@ -96,7 +96,7 @@ impl File {
modified_at: now, modified_at: now,
}) })
} }
/// Creates a folder entity /// Creates a folder entity
pub fn new_folder( pub fn new_folder(
id: String, id: String,
@@ -110,23 +110,23 @@ impl File {
if name.is_empty() || name.contains('/') || name.contains('\\') { if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name)); return Err(FileError::InvalidFileName(name));
} }
// Store the path string for serialization compatibility // Store the path string for serialization compatibility
let path_string = storage_path.to_string(); let path_string = storage_path.to_string();
Ok(Self { Ok(Self {
id, id,
name, name,
storage_path, storage_path,
path_string, path_string,
size: 0, // Folders have zero size size: 0, // Folders have zero size
mime_type: "directory".to_string(), // Standard MIME type for directories mime_type: "directory".to_string(), // Standard MIME type for directories
folder_id: parent_id, folder_id: parent_id,
created_at, created_at,
modified_at, modified_at,
}) })
} }
/// Creates a file with specific timestamps (for reconstruction) /// Creates a file with specific timestamps (for reconstruction)
pub fn with_timestamps( pub fn with_timestamps(
id: String, id: String,
@@ -142,10 +142,10 @@ impl File {
if name.is_empty() || name.contains('/') || name.contains('\\') { if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name)); return Err(FileError::InvalidFileName(name));
} }
// Store the path string for serialization compatibility // Store the path string for serialization compatibility
let path_string = storage_path.to_string(); let path_string = storage_path.to_string();
Ok(Self { Ok(Self {
id, id,
name, name,
@@ -158,44 +158,44 @@ impl File {
modified_at, modified_at,
}) })
} }
// Getters // Getters
pub fn id(&self) -> &str { pub fn id(&self) -> &str {
&self.id &self.id
} }
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }
pub fn storage_path(&self) -> &StoragePath { pub fn storage_path(&self) -> &StoragePath {
&self.storage_path &self.storage_path
} }
pub fn path_string(&self) -> &str { pub fn path_string(&self) -> &str {
&self.path_string &self.path_string
} }
pub fn size(&self) -> u64 { pub fn size(&self) -> u64 {
self.size self.size
} }
pub fn mime_type(&self) -> &str { pub fn mime_type(&self) -> &str {
&self.mime_type &self.mime_type
} }
pub fn folder_id(&self) -> Option<&str> { pub fn folder_id(&self) -> Option<&str> {
self.folder_id.as_deref() self.folder_id.as_deref()
} }
pub fn created_at(&self) -> u64 { pub fn created_at(&self) -> u64 {
self.created_at self.created_at
} }
pub fn modified_at(&self) -> u64 { pub fn modified_at(&self) -> u64 {
self.modified_at self.modified_at
} }
/// Creates a new File instance from a DTO /// Creates a new File instance from a DTO
/// This function is primarily for conversions in batch handlers /// This function is primarily for conversions in batch handlers
pub fn from_dto( pub fn from_dto(
@@ -210,7 +210,7 @@ impl File {
) -> Self { ) -> Self {
// Create storage_path from string // Create storage_path from string
let storage_path = StoragePath::from_string(&path); let storage_path = StoragePath::from_string(&path);
// Create directly without validation to avoid errors in DTO conversions // Create directly without validation to avoid errors in DTO conversions
Self { Self {
id, id,
@@ -224,31 +224,31 @@ impl File {
modified_at, modified_at,
} }
} }
// Methods to create new versions of the file (immutable) // Methods to create new versions of the file (immutable)
/// Creates a new version of the file with updated name /// Creates a new version of the file with updated name
pub fn with_name(&self, new_name: String) -> FileResult<Self> { pub fn with_name(&self, new_name: String) -> FileResult<Self> {
// Validate file name // Validate file name
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') { if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
return Err(FileError::InvalidFileName(new_name)); return Err(FileError::InvalidFileName(new_name));
} }
// Update path based on name // Update path based on name
let parent_path = self.storage_path.parent(); let parent_path = self.storage_path.parent();
let new_storage_path = match parent_path { let new_storage_path = match parent_path {
Some(parent) => parent.join(&new_name), Some(parent) => parent.join(&new_name),
None => StoragePath::from_string(&new_name), None => StoragePath::from_string(&new_name),
}; };
// Update string representation // Update string representation
let new_path_string = new_storage_path.to_string(); let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
.as_secs(); .as_secs();
Ok(Self { Ok(Self {
id: self.id.clone(), id: self.id.clone(),
name: new_name, name: new_name,
@@ -261,23 +261,27 @@ impl File {
modified_at: now, modified_at: now,
}) })
} }
/// Creates a new version of the file with updated folder /// Creates a new version of the file with updated folder
pub fn with_folder(&self, folder_id: Option<String>, folder_path: Option<StoragePath>) -> FileResult<Self> { pub fn with_folder(
&self,
folder_id: Option<String>,
folder_path: Option<StoragePath>,
) -> FileResult<Self> {
// We need a folder path to update the file path // We need a folder path to update the file path
let new_storage_path = match folder_path { let new_storage_path = match folder_path {
Some(path) => path.join(&self.name), Some(path) => path.join(&self.name),
None => StoragePath::from_string(&self.name), // Root None => StoragePath::from_string(&self.name), // Root
}; };
// Update string representation // Update string representation
let new_path_string = new_storage_path.to_string(); let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
.as_secs(); .as_secs();
Ok(Self { Ok(Self {
id: self.id.clone(), id: self.id.clone(),
name: self.name.clone(), name: self.name.clone(),
@@ -290,14 +294,14 @@ impl File {
modified_at: now, modified_at: now,
}) })
} }
/// Creates a new version of the file with updated size /// Creates a new version of the file with updated size
pub fn with_size(&self, new_size: u64) -> Self { pub fn with_size(&self, new_size: u64) -> Self {
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
.as_secs(); .as_secs();
Self { Self {
id: self.id.clone(), id: self.id.clone(),
name: self.name.clone(), name: self.name.clone(),
@@ -315,7 +319,7 @@ impl File {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_file_creation_with_valid_name() { fn test_file_creation_with_valid_name() {
let storage_path = StoragePath::from_string("/test/file.txt"); let storage_path = StoragePath::from_string("/test/file.txt");
@@ -327,10 +331,10 @@ mod tests {
"text/plain".to_string(), "text/plain".to_string(),
None, None,
); );
assert!(file.is_ok()); assert!(file.is_ok());
} }
#[test] #[test]
fn test_file_creation_with_invalid_name() { fn test_file_creation_with_invalid_name() {
let storage_path = StoragePath::from_string("/test/invalid/file.txt"); let storage_path = StoragePath::from_string("/test/invalid/file.txt");
@@ -342,14 +346,14 @@ mod tests {
"text/plain".to_string(), "text/plain".to_string(),
None, None,
); );
assert!(file.is_err()); assert!(file.is_err());
match file { match file {
Err(FileError::InvalidFileName(_)) => (), Err(FileError::InvalidFileName(_)) => (),
_ => panic!("Expected InvalidFileName error"), _ => panic!("Expected InvalidFileName error"),
} }
} }
#[test] #[test]
fn test_file_with_name() { fn test_file_with_name() {
let storage_path = StoragePath::from_string("/test/file.txt"); let storage_path = StoragePath::from_string("/test/file.txt");
@@ -360,12 +364,13 @@ mod tests {
100, 100,
"text/plain".to_string(), "text/plain".to_string(),
None, None,
).unwrap(); )
.unwrap();
let renamed = file.with_name("newname.txt".to_string()); let renamed = file.with_name("newname.txt".to_string());
assert!(renamed.is_ok()); assert!(renamed.is_ok());
let renamed = renamed.unwrap(); let renamed = renamed.unwrap();
assert_eq!(renamed.name(), "newname.txt"); assert_eq!(renamed.name(), "newname.txt");
assert_eq!(renamed.id(), "123"); // The ID does not change assert_eq!(renamed.id(), "123"); // The ID does not change
} }
} }
+49 -44
View File
@@ -8,22 +8,22 @@ pub use super::entity_errors::{FolderError, FolderResult};
pub struct Folder { pub struct Folder {
/// Unique identifier for the folder /// Unique identifier for the folder
id: String, id: String,
/// Name of the folder /// Name of the folder
name: String, name: String,
/// Path to the folder in the domain model /// Path to the folder in the domain model
storage_path: StoragePath, storage_path: StoragePath,
/// String representation of the path (for API compatibility) /// String representation of the path (for API compatibility)
path_string: String, path_string: String,
/// Parent folder ID (None if it's a root folder) /// Parent folder ID (None if it's a root folder)
parent_id: Option<String>, parent_id: Option<String>,
/// Creation timestamp /// Creation timestamp
created_at: u64, created_at: u64,
/// Last modification timestamp /// Last modification timestamp
modified_at: u64, modified_at: u64,
} }
@@ -56,15 +56,15 @@ impl Folder {
if name.is_empty() || name.contains('/') || name.contains('\\') { if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FolderError::InvalidFolderName(name)); return Err(FolderError::InvalidFolderName(name));
} }
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
.as_secs(); .as_secs();
// Store the path string for serialization compatibility // Store the path string for serialization compatibility
let path_string = storage_path.to_string(); let path_string = storage_path.to_string();
Ok(Self { Ok(Self {
id, id,
name, name,
@@ -75,7 +75,7 @@ impl Folder {
modified_at: now, modified_at: now,
}) })
} }
/// Creates a folder with specific timestamps (for reconstruction) /// Creates a folder with specific timestamps (for reconstruction)
pub fn with_timestamps( pub fn with_timestamps(
id: String, id: String,
@@ -89,10 +89,10 @@ impl Folder {
if name.is_empty() || name.contains('/') || name.contains('\\') { if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FolderError::InvalidFolderName(name)); return Err(FolderError::InvalidFolderName(name));
} }
// Store the path string for serialization compatibility // Store the path string for serialization compatibility
let path_string = storage_path.to_string(); let path_string = storage_path.to_string();
Ok(Self { Ok(Self {
id, id,
name, name,
@@ -103,36 +103,36 @@ impl Folder {
modified_at, modified_at,
}) })
} }
// Getters // Getters
pub fn id(&self) -> &str { pub fn id(&self) -> &str {
&self.id &self.id
} }
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }
pub fn storage_path(&self) -> &StoragePath { pub fn storage_path(&self) -> &StoragePath {
&self.storage_path &self.storage_path
} }
pub fn path_string(&self) -> &str { pub fn path_string(&self) -> &str {
&self.path_string &self.path_string
} }
pub fn parent_id(&self) -> Option<&str> { pub fn parent_id(&self) -> Option<&str> {
self.parent_id.as_deref() self.parent_id.as_deref()
} }
pub fn created_at(&self) -> u64 { pub fn created_at(&self) -> u64 {
self.created_at self.created_at
} }
pub fn modified_at(&self) -> u64 { pub fn modified_at(&self) -> u64 {
self.modified_at self.modified_at
} }
/// Creates a new Folder instance from a DTO /// Creates a new Folder instance from a DTO
/// This function is primarily for conversions in batch handlers /// This function is primarily for conversions in batch handlers
pub fn from_dto( pub fn from_dto(
@@ -145,7 +145,7 @@ impl Folder {
) -> Self { ) -> Self {
// Create storage_path from the string // Create storage_path from the string
let storage_path = StoragePath::from_string(&path); let storage_path = StoragePath::from_string(&path);
// Create directly without validation to avoid errors in DTO conversions // Create directly without validation to avoid errors in DTO conversions
Self { Self {
id, id,
@@ -157,31 +157,31 @@ impl Folder {
modified_at, modified_at,
} }
} }
// Methods to create new versions of the folder (immutable) // Methods to create new versions of the folder (immutable)
/// Creates a new version of the folder with updated name /// Creates a new version of the folder with updated name
pub fn with_name(&self, new_name: String) -> FolderResult<Self> { pub fn with_name(&self, new_name: String) -> FolderResult<Self> {
// Validate folder name // Validate folder name
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') { if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
return Err(FolderError::InvalidFolderName(new_name)); return Err(FolderError::InvalidFolderName(new_name));
} }
// Update path based on the name // Update path based on the name
let parent_path = self.storage_path.parent(); let parent_path = self.storage_path.parent();
let new_storage_path = match parent_path { let new_storage_path = match parent_path {
Some(parent) => parent.join(&new_name), Some(parent) => parent.join(&new_name),
None => StoragePath::from_string(&new_name), None => StoragePath::from_string(&new_name),
}; };
// Update string representation // Update string representation
let new_path_string = new_storage_path.to_string(); let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
.as_secs(); .as_secs();
Ok(Self { Ok(Self {
id: self.id.clone(), id: self.id.clone(),
name: new_name, name: new_name,
@@ -192,23 +192,27 @@ impl Folder {
modified_at: now, modified_at: now,
}) })
} }
/// Creates a new version of the folder with updated parent /// Creates a new version of the folder with updated parent
pub fn with_parent(&self, parent_id: Option<String>, parent_path: Option<StoragePath>) -> FolderResult<Self> { pub fn with_parent(
&self,
parent_id: Option<String>,
parent_path: Option<StoragePath>,
) -> FolderResult<Self> {
// We need a folder path to update the path // We need a folder path to update the path
let new_storage_path = match parent_path { let new_storage_path = match parent_path {
Some(path) => path.join(&self.name), Some(path) => path.join(&self.name),
None => StoragePath::from_string(&self.name), // Root None => StoragePath::from_string(&self.name), // Root
}; };
// Update string representation // Update string representation
let new_path_string = new_storage_path.to_string(); let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
.as_secs(); .as_secs();
Ok(Self { Ok(Self {
id: self.id.clone(), id: self.id.clone(),
name: self.name.clone(), name: self.name.clone(),
@@ -219,22 +223,22 @@ impl Folder {
modified_at: now, modified_at: now,
}) })
} }
/// Returns an absolute path for this folder /// Returns an absolute path for this folder
pub fn get_absolute_path<P: AsRef<std::path::Path>>(&self, root_path: P) -> std::path::PathBuf { pub fn get_absolute_path<P: AsRef<std::path::Path>>(&self, root_path: P) -> std::path::PathBuf {
let mut result = std::path::PathBuf::from(root_path.as_ref()); let mut result = std::path::PathBuf::from(root_path.as_ref());
// Skip leading '/' from path_string to avoid creating absolute path incorrectly // Skip leading '/' from path_string to avoid creating absolute path incorrectly
let relative_path = if self.path_string.starts_with('/') { let relative_path = if self.path_string.starts_with('/') {
&self.path_string[1..] &self.path_string[1..]
} else { } else {
&self.path_string &self.path_string
}; };
if !relative_path.is_empty() { if !relative_path.is_empty() {
result.push(relative_path); result.push(relative_path);
} }
result result
} }
} }
@@ -242,7 +246,7 @@ impl Folder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_folder_creation_with_valid_name() { fn test_folder_creation_with_valid_name() {
let storage_path = StoragePath::from_string("/test/folder"); let storage_path = StoragePath::from_string("/test/folder");
@@ -252,10 +256,10 @@ mod tests {
storage_path, storage_path,
None, None,
); );
assert!(folder.is_ok()); assert!(folder.is_ok());
} }
#[test] #[test]
fn test_folder_creation_with_invalid_name() { fn test_folder_creation_with_invalid_name() {
let storage_path = StoragePath::from_string("/test/invalid/folder"); let storage_path = StoragePath::from_string("/test/invalid/folder");
@@ -265,14 +269,14 @@ mod tests {
storage_path, storage_path,
None, None,
); );
assert!(folder.is_err()); assert!(folder.is_err());
match folder { match folder {
Err(FolderError::InvalidFolderName(_)) => (), Err(FolderError::InvalidFolderName(_)) => (),
_ => panic!("Expected InvalidFolderName error"), _ => panic!("Expected InvalidFolderName error"),
} }
} }
#[test] #[test]
fn test_folder_with_name() { fn test_folder_with_name() {
let storage_path = StoragePath::from_string("/test/folder"); let storage_path = StoragePath::from_string("/test/folder");
@@ -281,12 +285,13 @@ mod tests {
"old_name".to_string(), "old_name".to_string(),
storage_path, storage_path,
None, None,
).unwrap(); )
.unwrap();
let renamed = folder.with_name("new_name".to_string()); let renamed = folder.with_name("new_name".to_string());
assert!(renamed.is_ok()); assert!(renamed.is_ok());
let renamed = renamed.unwrap(); let renamed = renamed.unwrap();
assert_eq!(renamed.name(), "new_name"); assert_eq!(renamed.name(), "new_name");
assert_eq!(renamed.id(), "123"); // The ID doesn't change assert_eq!(renamed.id(), "123"); // The ID doesn't change
} }
} }
+4 -8
View File
@@ -4,17 +4,13 @@ pub mod contact;
pub mod entity_errors; pub mod entity_errors;
pub mod file; pub mod file;
pub mod folder; pub mod folder;
pub mod user;
pub mod session; pub mod session;
pub mod share; pub mod share;
pub mod trashed_item; pub mod trashed_item;
pub mod user;
// Re-exportar errores de entidades para facilitar el uso // Re-exportar errores de entidades para facilitar el uso
pub use entity_errors::{ pub use entity_errors::{
FileError, FileResult, CalendarError, CalendarEventError, CalendarEventResult, CalendarResult, FileError, FileResult,
FolderError, FolderResult, FolderError, FolderResult, ShareError, ShareResult, UserError, UserResult,
UserError, UserResult, };
ShareError, ShareResult,
CalendarError, CalendarResult,
CalendarEventError, CalendarEventResult,
};
+10 -10
View File
@@ -1,5 +1,5 @@
use chrono::{DateTime, Duration, Utc};
use uuid::Uuid; use uuid::Uuid;
use chrono::{DateTime, Utc, Duration};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Session { pub struct Session {
@@ -64,20 +64,20 @@ impl Session {
revoked, revoked,
} }
} }
// Getters // Getters
pub fn id(&self) -> &str { pub fn id(&self) -> &str {
&self.id &self.id
} }
pub fn user_id(&self) -> &str { pub fn user_id(&self) -> &str {
&self.user_id &self.user_id
} }
pub fn refresh_token(&self) -> &str { pub fn refresh_token(&self) -> &str {
&self.refresh_token &self.refresh_token
} }
pub fn expires_at(&self) -> DateTime<Utc> { pub fn expires_at(&self) -> DateTime<Utc> {
self.expires_at self.expires_at
} }
@@ -89,20 +89,20 @@ impl Session {
pub fn user_agent(&self) -> Option<&str> { pub fn user_agent(&self) -> Option<&str> {
self.user_agent.as_deref() self.user_agent.as_deref()
} }
pub fn created_at(&self) -> DateTime<Utc> { pub fn created_at(&self) -> DateTime<Utc> {
self.created_at self.created_at
} }
pub fn is_expired(&self) -> bool { pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at Utc::now() > self.expires_at
} }
pub fn is_revoked(&self) -> bool { pub fn is_revoked(&self) -> bool {
self.revoked self.revoked
} }
pub fn revoke(&mut self) { pub fn revoke(&mut self) {
self.revoked = true; self.revoked = true;
} }
} }
+37 -21
View File
@@ -33,7 +33,7 @@ pub enum ShareItemType {
impl Share { impl Share {
pub fn new( pub fn new(
item_id: String, item_id: String,
item_type: ShareItemType, item_type: ShareItemType,
created_by: String, created_by: String,
permissions: Option<SharePermissions>, permissions: Option<SharePermissions>,
@@ -42,7 +42,9 @@ impl Share {
) -> Result<Self, ShareError> { ) -> Result<Self, ShareError> {
// Validate item_id // Validate item_id
if item_id.is_empty() { if item_id.is_empty() {
return Err(ShareError::ValidationError("Item ID cannot be empty".to_string())); return Err(ShareError::ValidationError(
"Item ID cannot be empty".to_string(),
));
} }
// Validate expiration date if provided // Validate expiration date if provided
@@ -51,9 +53,11 @@ impl Share {
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.expect("Time went backwards") .expect("Time went backwards")
.as_secs(); .as_secs();
if expires <= now { if expires <= now {
return Err(ShareError::InvalidExpiration("Expiration date must be in the future".to_string())); return Err(ShareError::InvalidExpiration(
"Expiration date must be in the future".to_string(),
));
} }
} }
@@ -174,10 +178,10 @@ impl Share {
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.expect("Time went backwards") .expect("Time went backwards")
.as_secs(); .as_secs();
return expires_at <= now; return expires_at <= now;
} }
false false
} }
@@ -192,7 +196,7 @@ impl Share {
} }
/// Returns a reference to the password hash, if one is set. /// Returns a reference to the password hash, if one is set.
/// ///
/// Password verification should be performed externally via PasswordHasherPort /// Password verification should be performed externally via PasswordHasherPort
/// to keep cryptographic dependencies out of the domain layer. /// to keep cryptographic dependencies out of the domain layer.
pub fn password_hash(&self) -> Option<&str> { pub fn password_hash(&self) -> Option<&str> {
@@ -238,7 +242,10 @@ impl TryFrom<&str> for ShareItemType {
match s.to_lowercase().as_str() { match s.to_lowercase().as_str() {
"file" => Ok(ShareItemType::File), "file" => Ok(ShareItemType::File),
"folder" => Ok(ShareItemType::Folder), "folder" => Ok(ShareItemType::Folder),
_ => Err(ShareError::ValidationError(format!("Invalid item type: {}", s))), _ => Err(ShareError::ValidationError(format!(
"Invalid item type: {}",
s
))),
} }
} }
} }
@@ -276,7 +283,7 @@ mod tests {
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.expect("Time went backwards") .expect("Time went backwards")
.as_secs(); .as_secs();
// Create a share that expires in the future // Create a share that expires in the future
let future = now + 3600; // 1 hour in the future let future = now + 3600; // 1 hour in the future
let share = Share::new( let share = Share::new(
@@ -288,9 +295,9 @@ mod tests {
Some(future), Some(future),
) )
.unwrap(); .unwrap();
assert!(!share.is_expired()); assert!(!share.is_expired());
// Test with past expiration (should fail during creation) // Test with past expiration (should fail during creation)
let past = now - 3600; // 1 hour in the past let past = now - 3600; // 1 hour in the past
let share_result = Share::new( let share_result = Share::new(
@@ -301,21 +308,30 @@ mod tests {
None, None,
Some(past), Some(past),
); );
assert!(share_result.is_err()); assert!(share_result.is_err());
} }
#[test] #[test]
fn test_share_item_type_conversion() { fn test_share_item_type_conversion() {
assert_eq!(ShareItemType::File.to_string(), "file"); assert_eq!(ShareItemType::File.to_string(), "file");
assert_eq!(ShareItemType::Folder.to_string(), "folder"); assert_eq!(ShareItemType::Folder.to_string(), "folder");
assert_eq!(ShareItemType::try_from("file").unwrap(), ShareItemType::File); assert_eq!(
assert_eq!(ShareItemType::try_from("folder").unwrap(), ShareItemType::Folder); ShareItemType::try_from("file").unwrap(),
assert_eq!(ShareItemType::try_from("FILE").unwrap(), ShareItemType::File); ShareItemType::File
);
assert_eq!(
ShareItemType::try_from("folder").unwrap(),
ShareItemType::Folder
);
assert_eq!(
ShareItemType::try_from("FILE").unwrap(),
ShareItemType::File
);
assert!(ShareItemType::try_from("invalid").is_err()); assert!(ShareItemType::try_from("invalid").is_err());
} }
#[test] #[test]
fn test_has_password_with_hash() { fn test_has_password_with_hash() {
let share = Share::new( let share = Share::new(
@@ -327,11 +343,11 @@ mod tests {
None, None,
) )
.unwrap(); .unwrap();
assert!(share.has_password()); assert!(share.has_password());
assert_eq!(share.password_hash(), Some("some_hash_value")); assert_eq!(share.password_hash(), Some("some_hash_value"));
} }
#[test] #[test]
fn test_has_password_without_hash() { fn test_has_password_without_hash() {
let share = Share::new( let share = Share::new(
@@ -343,7 +359,7 @@ mod tests {
None, None,
) )
.unwrap(); .unwrap();
assert!(!share.has_password()); assert!(!share.has_password());
assert_eq!(share.password_hash(), None); assert_eq!(share.password_hash(), None);
} }
+1 -1
View File
@@ -103,4 +103,4 @@ impl TrashedItem {
let now = Utc::now(); let now = Utc::now();
(self.deletion_date - now).num_days().max(0) (self.deletion_date - now).num_days().max(0)
} }
} }
+36 -34
View File
@@ -1,5 +1,5 @@
use uuid::Uuid;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use uuid::Uuid;
// Re-export entity errors from the centralized module // Re-export entity errors from the centralized module
pub use super::entity_errors::{UserError, UserResult}; pub use super::entity_errors::{UserError, UserResult};
@@ -23,7 +23,7 @@ impl std::fmt::Display for UserRole {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct User { pub struct User {
id: String, id: String,
username: String, username: String,
email: String, email: String,
password_hash: String, password_hash: String,
role: UserRole, role: UserRole,
@@ -39,11 +39,11 @@ pub struct User {
impl User { impl User {
/// Create a new user with a pre-hashed password. /// Create a new user with a pre-hashed password.
/// ///
/// The password hashing should be done externally using PasswordHasherPort /// The password hashing should be done externally using PasswordHasherPort
/// to maintain clean architecture and keep cryptographic dependencies /// to maintain clean architecture and keep cryptographic dependencies
/// out of the domain layer. /// out of the domain layer.
/// ///
/// # Arguments /// # Arguments
/// * `username` - User's username (3-32 characters) /// * `username` - User's username (3-32 characters)
/// * `email` - User's email address /// * `email` - User's email address
@@ -52,26 +52,30 @@ impl User {
/// * `storage_quota_bytes` - Storage quota in bytes /// * `storage_quota_bytes` - Storage quota in bytes
pub fn new( pub fn new(
username: String, username: String,
email: String, email: String,
password_hash: String, password_hash: String,
role: UserRole, role: UserRole,
storage_quota_bytes: i64, storage_quota_bytes: i64,
) -> UserResult<Self> { ) -> UserResult<Self> {
// Validations // Validations
if username.is_empty() || username.len() < 3 || username.len() > 32 { if username.is_empty() || username.len() < 3 || username.len() > 32 {
return Err(UserError::InvalidUsername("Username must be between 3 and 32 characters".to_string())); return Err(UserError::InvalidUsername(
"Username must be between 3 and 32 characters".to_string(),
));
} }
if !email.contains('@') || email.len() < 5 { if !email.contains('@') || email.len() < 5 {
return Err(UserError::ValidationError("Invalid email".to_string())); return Err(UserError::ValidationError("Invalid email".to_string()));
} }
if password_hash.is_empty() { if password_hash.is_empty() {
return Err(UserError::InvalidPassword("Password hash cannot be empty".to_string())); return Err(UserError::InvalidPassword(
"Password hash cannot be empty".to_string(),
));
} }
let now = Utc::now(); let now = Utc::now();
Ok(Self { Ok(Self {
id: Uuid::new_v4().to_string(), id: Uuid::new_v4().to_string(),
username, username,
@@ -88,7 +92,7 @@ impl User {
oidc_subject: None, oidc_subject: None,
}) })
} }
/// Create a new OIDC-authenticated user (no password required). /// Create a new OIDC-authenticated user (no password required).
pub fn new_oidc( pub fn new_oidc(
username: String, username: String,
@@ -104,9 +108,7 @@ impl User {
)); ));
} }
if !email.contains('@') || email.len() < 5 { if !email.contains('@') || email.len() < 5 {
return Err(UserError::ValidationError( return Err(UserError::ValidationError("Invalid email".to_string()));
"Invalid email".to_string(),
));
} }
let now = Utc::now(); let now = Utc::now();
Ok(Self { Ok(Self {
@@ -125,7 +127,7 @@ impl User {
oidc_subject: Some(oidc_subject), oidc_subject: Some(oidc_subject),
}) })
} }
// Create from existing values (for reconstruction from DB) // Create from existing values (for reconstruction from DB)
pub fn from_data( pub fn from_data(
id: String, id: String,
@@ -189,48 +191,48 @@ impl User {
oidc_subject, oidc_subject,
} }
} }
// Getters // Getters
pub fn id(&self) -> &str { pub fn id(&self) -> &str {
&self.id &self.id
} }
pub fn username(&self) -> &str { pub fn username(&self) -> &str {
&self.username &self.username
} }
pub fn email(&self) -> &str { pub fn email(&self) -> &str {
&self.email &self.email
} }
pub fn role(&self) -> UserRole { pub fn role(&self) -> UserRole {
self.role self.role
} }
pub fn storage_quota_bytes(&self) -> i64 { pub fn storage_quota_bytes(&self) -> i64 {
self.storage_quota_bytes self.storage_quota_bytes
} }
pub fn storage_used_bytes(&self) -> i64 { pub fn storage_used_bytes(&self) -> i64 {
self.storage_used_bytes self.storage_used_bytes
} }
pub fn created_at(&self) -> DateTime<Utc> { pub fn created_at(&self) -> DateTime<Utc> {
self.created_at self.created_at
} }
pub fn updated_at(&self) -> DateTime<Utc> { pub fn updated_at(&self) -> DateTime<Utc> {
self.updated_at self.updated_at
} }
pub fn last_login_at(&self) -> Option<DateTime<Utc>> { pub fn last_login_at(&self) -> Option<DateTime<Utc>> {
self.last_login_at self.last_login_at
} }
pub fn is_active(&self) -> bool { pub fn is_active(&self) -> bool {
self.active self.active
} }
pub fn password_hash(&self) -> &str { pub fn password_hash(&self) -> &str {
&self.password_hash &self.password_hash
} }
@@ -247,38 +249,38 @@ impl User {
pub fn is_oidc_user(&self) -> bool { pub fn is_oidc_user(&self) -> bool {
self.oidc_provider.is_some() self.oidc_provider.is_some()
} }
/// Update the password hash. /// Update the password hash.
/// ///
/// The new password should be hashed externally using PasswordHasherPort /// The new password should be hashed externally using PasswordHasherPort
/// before calling this method. /// before calling this method.
pub fn update_password_hash(&mut self, new_hash: String) { pub fn update_password_hash(&mut self, new_hash: String) {
self.password_hash = new_hash; self.password_hash = new_hash;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
// Update storage usage // Update storage usage
pub fn update_storage_used(&mut self, storage_used_bytes: i64) { pub fn update_storage_used(&mut self, storage_used_bytes: i64) {
self.storage_used_bytes = storage_used_bytes; self.storage_used_bytes = storage_used_bytes;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
// Register login // Register login
pub fn register_login(&mut self) { pub fn register_login(&mut self) {
let now = Utc::now(); let now = Utc::now();
self.last_login_at = Some(now); self.last_login_at = Some(now);
self.updated_at = now; self.updated_at = now;
} }
// Deactivate user // Deactivate user
pub fn deactivate(&mut self) { pub fn deactivate(&mut self) {
self.active = false; self.active = false;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
// Activate user // Activate user
pub fn activate(&mut self) { pub fn activate(&mut self) {
self.active = true; self.active = true;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
} }
+267 -271
View File
@@ -1,271 +1,267 @@
//! Domain errors //! Domain errors
//! //!
//! This module contains domain-specific error types. //! This module contains domain-specific error types.
//! DomainError is the base error used throughout the domain layer. //! DomainError is the base error used throughout the domain layer.
use std::fmt::{Display, Formatter, Result as FmtResult}; use std::error::Error as StdError;
use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult};
use thiserror::Error; use thiserror::Error;
/// Common Result type for the domain with DomainError as the standard error /// Common Result type for the domain with DomainError as the standard error
pub type Result<T> = std::result::Result<T, DomainError>; pub type Result<T> = std::result::Result<T, DomainError>;
/// Domain error types /// Domain error types
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind { pub enum ErrorKind {
/// Entity not found /// Entity not found
NotFound, NotFound,
/// Entity already exists /// Entity already exists
AlreadyExists, AlreadyExists,
/// Invalid input or failed validation /// Invalid input or failed validation
InvalidInput, InvalidInput,
/// Access or permissions error /// Access or permissions error
AccessDenied, AccessDenied,
/// Timeout expired /// Timeout expired
Timeout, Timeout,
/// Internal system error /// Internal system error
InternalError, InternalError,
/// Functionality not implemented /// Functionality not implemented
NotImplemented, NotImplemented,
/// Unsupported operation /// Unsupported operation
UnsupportedOperation, UnsupportedOperation,
/// Database error /// Database error
DatabaseError, DatabaseError,
} }
impl Display for ErrorKind { impl Display for ErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self { match self {
ErrorKind::NotFound => write!(f, "Not Found"), ErrorKind::NotFound => write!(f, "Not Found"),
ErrorKind::AlreadyExists => write!(f, "Already Exists"), ErrorKind::AlreadyExists => write!(f, "Already Exists"),
ErrorKind::InvalidInput => write!(f, "Invalid Input"), ErrorKind::InvalidInput => write!(f, "Invalid Input"),
ErrorKind::AccessDenied => write!(f, "Access Denied"), ErrorKind::AccessDenied => write!(f, "Access Denied"),
ErrorKind::Timeout => write!(f, "Timeout"), ErrorKind::Timeout => write!(f, "Timeout"),
ErrorKind::InternalError => write!(f, "Internal Error"), ErrorKind::InternalError => write!(f, "Internal Error"),
ErrorKind::NotImplemented => write!(f, "Not Implemented"), ErrorKind::NotImplemented => write!(f, "Not Implemented"),
ErrorKind::UnsupportedOperation => write!(f, "Unsupported Operation"), ErrorKind::UnsupportedOperation => write!(f, "Unsupported Operation"),
ErrorKind::DatabaseError => write!(f, "Database Error"), ErrorKind::DatabaseError => write!(f, "Database Error"),
} }
} }
} }
/// Base domain error that provides detailed context /// Base domain error that provides detailed context
#[derive(Error, Debug)] #[derive(Error, Debug)]
#[error("{kind}: {message}")] #[error("{kind}: {message}")]
pub struct DomainError { pub struct DomainError {
/// Error type /// Error type
pub kind: ErrorKind, pub kind: ErrorKind,
/// Affected entity type (e.g.: "File", "Folder") /// Affected entity type (e.g.: "File", "Folder")
pub entity_type: &'static str, pub entity_type: &'static str,
/// Entity identifier if available /// Entity identifier if available
pub entity_id: Option<String>, pub entity_id: Option<String>,
/// Descriptive error message /// Descriptive error message
pub message: String, pub message: String,
/// Source error (optional) /// Source error (optional)
#[source] #[source]
pub source: Option<Box<dyn StdError + Send + Sync>>, pub source: Option<Box<dyn StdError + Send + Sync>>,
} }
impl DomainError { impl DomainError {
/// Creates a new domain error /// Creates a new domain error
pub fn new<S: Into<String>>( pub fn new<S: Into<String>>(kind: ErrorKind, entity_type: &'static str, message: S) -> Self {
kind: ErrorKind, Self {
entity_type: &'static str, kind,
message: S, entity_type,
) -> Self { entity_id: None,
Self { message: message.into(),
kind, source: None,
entity_type, }
entity_id: None, }
message: message.into(),
source: None, /// Creates an entity not found error
} pub fn not_found<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
} let id = entity_id.into();
Self {
/// Creates an entity not found error kind: ErrorKind::NotFound,
pub fn not_found<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self { entity_type,
let id = entity_id.into(); entity_id: Some(id.clone()),
Self { message: format!("{} not found: {}", entity_type, id),
kind: ErrorKind::NotFound, source: None,
entity_type, }
entity_id: Some(id.clone()), }
message: format!("{} not found: {}", entity_type, id),
source: None, /// Creates an entity already exists error
} pub fn already_exists<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
} let id = entity_id.into();
Self {
/// Creates an entity already exists error kind: ErrorKind::AlreadyExists,
pub fn already_exists<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self { entity_type,
let id = entity_id.into(); entity_id: Some(id.clone()),
Self { message: format!("{} already exists: {}", entity_type, id),
kind: ErrorKind::AlreadyExists, source: None,
entity_type, }
entity_id: Some(id.clone()), }
message: format!("{} already exists: {}", entity_type, id),
source: None, /// Creates an error for unsupported operations
} pub fn operation_not_supported<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
} Self::new(ErrorKind::UnsupportedOperation, entity_type, message)
}
/// Creates an error for unsupported operations
pub fn operation_not_supported<S: Into<String>>(entity_type: &'static str, message: S) -> Self { /// Creates a timeout error
Self::new( pub fn timeout<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
ErrorKind::UnsupportedOperation, Self {
entity_type, kind: ErrorKind::Timeout,
message, entity_type,
) entity_id: None,
} message: message.into(),
source: None,
/// Creates a timeout error }
pub fn timeout<S: Into<String>>(entity_type: &'static str, message: S) -> Self { }
Self {
kind: ErrorKind::Timeout, /// Creates an internal error
entity_type, pub fn internal_error<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
entity_id: None, Self {
message: message.into(), kind: ErrorKind::InternalError,
source: None, entity_type,
} entity_id: None,
} message: message.into(),
source: None,
/// Creates an internal error }
pub fn internal_error<S: Into<String>>(entity_type: &'static str, message: S) -> Self { }
Self {
kind: ErrorKind::InternalError, /// Creates an access denied error
entity_type, pub fn access_denied<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
entity_id: None, Self {
message: message.into(), kind: ErrorKind::AccessDenied,
source: None, entity_type,
} entity_id: None,
} message: message.into(),
source: None,
/// Creates an access denied error }
pub fn access_denied<S: Into<String>>(entity_type: &'static str, message: S) -> Self { }
Self {
kind: ErrorKind::AccessDenied, /// Alias for access_denied to maintain compatibility
entity_type, pub fn unauthorized<S: Into<String>>(message: S) -> Self {
entity_id: None, Self {
message: message.into(), kind: ErrorKind::AccessDenied,
source: None, entity_type: "Authorization",
} entity_id: None,
} message: message.into(),
source: None,
/// Alias for access_denied to maintain compatibility }
pub fn unauthorized<S: Into<String>>(message: S) -> Self { }
Self {
kind: ErrorKind::AccessDenied, /// Creates a database error
entity_type: "Authorization", pub fn database_error<S: Into<String>>(message: S) -> Self {
entity_id: None, Self {
message: message.into(), kind: ErrorKind::DatabaseError,
source: None, entity_type: "Database",
} entity_id: None,
} message: message.into(),
source: None,
/// Creates a database error }
pub fn database_error<S: Into<String>>(message: S) -> Self { }
Self {
kind: ErrorKind::DatabaseError, /// Creates a validation error
entity_type: "Database", pub fn validation_error<S: Into<String>>(message: S) -> Self {
entity_id: None, Self {
message: message.into(), kind: ErrorKind::InvalidInput,
source: None, entity_type: "Validation",
} entity_id: None,
} message: message.into(),
source: None,
/// Creates a validation error }
pub fn validation_error<S: Into<String>>(message: S) -> Self { }
Self {
kind: ErrorKind::InvalidInput, /// Creates a not implemented error
entity_type: "Validation", pub fn not_implemented<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
entity_id: None, Self {
message: message.into(), kind: ErrorKind::NotImplemented,
source: None, entity_type,
} entity_id: None,
} message: message.into(),
source: None,
/// Creates a not implemented error }
pub fn not_implemented<S: Into<String>>(entity_type: &'static str, message: S) -> Self { }
Self {
kind: ErrorKind::NotImplemented, /// Sets the entity ID
entity_type, pub fn with_id<S: Into<String>>(mut self, entity_id: S) -> Self {
entity_id: None, self.entity_id = Some(entity_id.into());
message: message.into(), self
source: None, }
}
} /// Sets the source error
pub fn with_source<E: StdError + Send + Sync + 'static>(mut self, source: E) -> Self {
/// Sets the entity ID self.source = Some(Box::new(source));
pub fn with_id<S: Into<String>>(mut self, entity_id: S) -> Self { self
self.entity_id = Some(entity_id.into()); }
self }
}
/// Trait for adding context to errors
/// Sets the source error pub trait ErrorContext<T, E> {
pub fn with_source<E: StdError + Send + Sync + 'static>(mut self, source: E) -> Self { fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
self.source = Some(Box::new(source)); where
self C: Into<String>,
} F: FnOnce() -> C;
}
fn with_error_kind(
/// Trait for adding context to errors self,
pub trait ErrorContext<T, E> { kind: ErrorKind,
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError> entity_type: &'static str,
where ) -> std::result::Result<T, DomainError>;
C: Into<String>, }
F: FnOnce() -> C;
impl<T, E: StdError + Send + Sync + 'static> ErrorContext<T, E> for std::result::Result<T, E> {
fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result<T, DomainError>; fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
} where
C: Into<String>,
impl<T, E: StdError + Send + Sync + 'static> ErrorContext<T, E> for std::result::Result<T, E> { F: FnOnce() -> C,
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError> {
where self.map_err(|e| DomainError {
C: Into<String>, kind: ErrorKind::InternalError,
F: FnOnce() -> C, entity_type: "Unknown",
{ entity_id: None,
self.map_err(|e| { message: context().into(),
DomainError { source: Some(Box::new(e)),
kind: ErrorKind::InternalError, })
entity_type: "Unknown", }
entity_id: None,
message: context().into(), fn with_error_kind(
source: Some(Box::new(e)), self,
} kind: ErrorKind,
}) entity_type: &'static str,
} ) -> std::result::Result<T, DomainError> {
self.map_err(|e| DomainError {
fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result<T, DomainError> { kind,
self.map_err(|e| { entity_type,
DomainError { entity_id: None,
kind, message: format!("{}", e),
entity_type, source: Some(Box::new(e)),
entity_id: None, })
message: format!("{}", e), }
source: Some(Box::new(e)), }
}
}) // From implementations for standard errors (without external infrastructure dependencies)
} impl From<std::io::Error> for DomainError {
} fn from(err: std::io::Error) -> Self {
DomainError {
// From implementations for standard errors (without external infrastructure dependencies) kind: ErrorKind::InternalError,
impl From<std::io::Error> for DomainError { entity_type: "IO",
fn from(err: std::io::Error) -> Self { entity_id: None,
DomainError { message: format!("{}", err),
kind: ErrorKind::InternalError, source: Some(Box::new(err)),
entity_type: "IO", }
entity_id: None, }
message: format!("{}", err), }
source: Some(Box::new(err)),
} impl From<uuid::Error> for DomainError {
} fn from(err: uuid::Error) -> Self {
} DomainError {
kind: ErrorKind::InvalidInput,
impl From<uuid::Error> for DomainError { entity_type: "UUID",
fn from(err: uuid::Error) -> Self { entity_id: None,
DomainError { message: format!("{}", err),
kind: ErrorKind::InvalidInput, source: Some(Box::new(err)),
entity_type: "UUID", }
entity_id: None, }
message: format!("{}", err), }
source: Some(Box::new(err)),
}
}
}
@@ -1,6 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use uuid::Uuid;
use std::result::Result; use std::result::Result;
use uuid::Uuid;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use crate::domain::entities::contact::AddressBook; use crate::domain::entities::contact::AddressBook;
@@ -9,14 +9,41 @@ pub type AddressBookRepositoryResult<T> = Result<T, DomainError>;
#[async_trait] #[async_trait]
pub trait AddressBookRepository: Send + Sync + 'static { pub trait AddressBookRepository: Send + Sync + 'static {
async fn create_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook>; async fn create_address_book(
async fn update_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook>; &self,
address_book: AddressBook,
) -> AddressBookRepositoryResult<AddressBook>;
async fn update_address_book(
&self,
address_book: AddressBook,
) -> AddressBookRepositoryResult<AddressBook>;
async fn delete_address_book(&self, id: &Uuid) -> AddressBookRepositoryResult<()>; async fn delete_address_book(&self, id: &Uuid) -> AddressBookRepositoryResult<()>;
async fn get_address_book_by_id(&self, id: &Uuid) -> AddressBookRepositoryResult<Option<AddressBook>>; async fn get_address_book_by_id(
async fn get_address_books_by_owner(&self, owner_id: &str) -> AddressBookRepositoryResult<Vec<AddressBook>>; &self,
async fn get_shared_address_books(&self, user_id: &str) -> AddressBookRepositoryResult<Vec<AddressBook>>; id: &Uuid,
) -> AddressBookRepositoryResult<Option<AddressBook>>;
async fn get_address_books_by_owner(
&self,
owner_id: &str,
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn get_shared_address_books(
&self,
user_id: &str,
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn get_public_address_books(&self) -> AddressBookRepositoryResult<Vec<AddressBook>>; async fn get_public_address_books(&self) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn share_address_book(&self, address_book_id: &Uuid, user_id: &str, can_write: bool) -> AddressBookRepositoryResult<()>; async fn share_address_book(
async fn unshare_address_book(&self, address_book_id: &Uuid, user_id: &str) -> AddressBookRepositoryResult<()>; &self,
async fn get_address_book_shares(&self, address_book_id: &Uuid) -> AddressBookRepositoryResult<Vec<(String, bool)>>; address_book_id: &Uuid,
} user_id: &str,
can_write: bool,
) -> AddressBookRepositoryResult<()>;
async fn unshare_address_book(
&self,
address_book_id: &Uuid,
user_id: &str,
) -> AddressBookRepositoryResult<()>;
async fn get_address_book_shares(
&self,
address_book_id: &Uuid,
) -> AddressBookRepositoryResult<Vec<(String, bool)>>;
}
@@ -1,8 +1,8 @@
use async_trait::async_trait;
use uuid::Uuid;
use chrono::{DateTime, Utc};
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use crate::domain::entities::calendar_event::CalendarEvent; use crate::domain::entities::calendar_event::CalendarEvent;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use uuid::Uuid;
pub type CalendarEventRepositoryResult<T> = Result<T, DomainError>; pub type CalendarEventRepositoryResult<T> = Result<T, DomainError>;
@@ -10,53 +10,76 @@ pub type CalendarEventRepositoryResult<T> = Result<T, DomainError>;
#[async_trait] #[async_trait]
pub trait CalendarEventRepository: Send + Sync + 'static { pub trait CalendarEventRepository: Send + Sync + 'static {
/// Creates a new calendar event /// Creates a new calendar event
async fn create_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent>; async fn create_event(
&self,
event: CalendarEvent,
) -> CalendarEventRepositoryResult<CalendarEvent>;
/// Updates an existing calendar event /// Updates an existing calendar event
async fn update_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent>; async fn update_event(
&self,
event: CalendarEvent,
) -> CalendarEventRepositoryResult<CalendarEvent>;
/// Deletes a calendar event by ID /// Deletes a calendar event by ID
async fn delete_event(&self, id: &Uuid) -> CalendarEventRepositoryResult<()>; async fn delete_event(&self, id: &Uuid) -> CalendarEventRepositoryResult<()>;
/// Finds a calendar event by its ID /// Finds a calendar event by its ID
async fn find_event_by_id(&self, id: &Uuid) -> CalendarEventRepositoryResult<CalendarEvent>; async fn find_event_by_id(&self, id: &Uuid) -> CalendarEventRepositoryResult<CalendarEvent>;
/// Lists all events in a specific calendar /// Lists all events in a specific calendar
async fn list_events_by_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>; async fn list_events_by_calendar(
&self,
calendar_id: &Uuid,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
/// Finds events in a calendar by their summary/title (partial match) /// Finds events in a calendar by their summary/title (partial match)
async fn find_events_by_summary(&self, calendar_id: &Uuid, summary: &str) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>; async fn find_events_by_summary(
&self,
calendar_id: &Uuid,
summary: &str,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
/// Gets events in a specific time range for a calendar /// Gets events in a specific time range for a calendar
async fn get_events_in_time_range( async fn get_events_in_time_range(
&self, &self,
calendar_id: &Uuid, calendar_id: &Uuid,
start: &DateTime<Utc>, start: &DateTime<Utc>,
end: &DateTime<Utc> end: &DateTime<Utc>,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>; ) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
/// Finds an event by its iCalendar UID in a specific calendar /// Finds an event by its iCalendar UID in a specific calendar
async fn find_event_by_ical_uid(&self, calendar_id: &Uuid, ical_uid: &str) -> CalendarEventRepositoryResult<Option<CalendarEvent>>; async fn find_event_by_ical_uid(
&self,
calendar_id: &Uuid,
ical_uid: &str,
) -> CalendarEventRepositoryResult<Option<CalendarEvent>>;
/// Counts events in a calendar /// Counts events in a calendar
async fn count_events_in_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<i64>; async fn count_events_in_calendar(
&self,
calendar_id: &Uuid,
) -> CalendarEventRepositoryResult<i64>;
/// Deletes all events in a calendar /// Deletes all events in a calendar
async fn delete_all_events_in_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<i64>; async fn delete_all_events_in_calendar(
&self,
calendar_id: &Uuid,
) -> CalendarEventRepositoryResult<i64>;
/// Lists events by calendar with pagination /// Lists events by calendar with pagination
async fn list_events_by_calendar_paginated( async fn list_events_by_calendar_paginated(
&self, &self,
calendar_id: &Uuid, calendar_id: &Uuid,
limit: i64, limit: i64,
offset: i64 offset: i64,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>; ) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
/// Finds events with recurrence rules that might occur in a time range /// Finds events with recurrence rules that might occur in a time range
async fn find_recurring_events_in_range( async fn find_recurring_events_in_range(
&self, &self,
calendar_id: &Uuid, calendar_id: &Uuid,
start: &DateTime<Utc>, start: &DateTime<Utc>,
end: &DateTime<Utc> end: &DateTime<Utc>,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>; ) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
} }
+76 -30
View File
@@ -1,7 +1,7 @@
use async_trait::async_trait;
use uuid::Uuid;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use crate::domain::entities::calendar::Calendar; use crate::domain::entities::calendar::Calendar;
use async_trait::async_trait;
use uuid::Uuid;
pub type CalendarRepositoryResult<T> = Result<T, DomainError>; pub type CalendarRepositoryResult<T> = Result<T, DomainError>;
@@ -10,49 +10,95 @@ pub type CalendarRepositoryResult<T> = Result<T, DomainError>;
pub trait CalendarRepository: Send + Sync + 'static { pub trait CalendarRepository: Send + Sync + 'static {
/// Creates a new calendar /// Creates a new calendar
async fn create_calendar(&self, calendar: Calendar) -> CalendarRepositoryResult<Calendar>; async fn create_calendar(&self, calendar: Calendar) -> CalendarRepositoryResult<Calendar>;
/// Updates an existing calendar /// Updates an existing calendar
async fn update_calendar(&self, calendar: Calendar) -> CalendarRepositoryResult<Calendar>; async fn update_calendar(&self, calendar: Calendar) -> CalendarRepositoryResult<Calendar>;
/// Deletes a calendar by ID /// Deletes a calendar by ID
async fn delete_calendar(&self, id: &Uuid) -> CalendarRepositoryResult<()>; async fn delete_calendar(&self, id: &Uuid) -> CalendarRepositoryResult<()>;
/// Finds a calendar by its ID /// Finds a calendar by its ID
async fn find_calendar_by_id(&self, id: &Uuid) -> CalendarRepositoryResult<Calendar>; async fn find_calendar_by_id(&self, id: &Uuid) -> CalendarRepositoryResult<Calendar>;
/// Lists all calendars for a specific user /// Lists all calendars for a specific user
async fn list_calendars_by_owner(&self, owner_id: &str) -> CalendarRepositoryResult<Vec<Calendar>>; async fn list_calendars_by_owner(
&self,
owner_id: &str,
) -> CalendarRepositoryResult<Vec<Calendar>>;
/// Finds a calendar by name and owner /// Finds a calendar by name and owner
async fn find_calendar_by_name_and_owner(&self, name: &str, owner_id: &str) -> CalendarRepositoryResult<Calendar>; async fn find_calendar_by_name_and_owner(
&self,
name: &str,
owner_id: &str,
) -> CalendarRepositoryResult<Calendar>;
/// Lists calendars shared with a specific user /// Lists calendars shared with a specific user
async fn list_calendars_shared_with_user(&self, user_id: &str) -> CalendarRepositoryResult<Vec<Calendar>>; async fn list_calendars_shared_with_user(
&self,
user_id: &str,
) -> CalendarRepositoryResult<Vec<Calendar>>;
/// List public calendars /// List public calendars
async fn list_public_calendars(&self, limit: i64, offset: i64) -> CalendarRepositoryResult<Vec<Calendar>>; async fn list_public_calendars(
&self,
limit: i64,
offset: i64,
) -> CalendarRepositoryResult<Vec<Calendar>>;
/// Checks if a user has access to a calendar /// Checks if a user has access to a calendar
async fn user_has_calendar_access(&self, calendar_id: &Uuid, user_id: &str) -> CalendarRepositoryResult<bool>; async fn user_has_calendar_access(
&self,
calendar_id: &Uuid,
user_id: &str,
) -> CalendarRepositoryResult<bool>;
/// Gets a custom property for a calendar /// Gets a custom property for a calendar
async fn get_calendar_property(&self, calendar_id: &Uuid, property_name: &str) -> CalendarRepositoryResult<Option<String>>; async fn get_calendar_property(
&self,
calendar_id: &Uuid,
property_name: &str,
) -> CalendarRepositoryResult<Option<String>>;
/// Sets a custom property for a calendar /// Sets a custom property for a calendar
async fn set_calendar_property(&self, calendar_id: &Uuid, property_name: &str, property_value: &str) -> CalendarRepositoryResult<()>; async fn set_calendar_property(
&self,
calendar_id: &Uuid,
property_name: &str,
property_value: &str,
) -> CalendarRepositoryResult<()>;
/// Removes a custom property from a calendar /// Removes a custom property from a calendar
async fn remove_calendar_property(&self, calendar_id: &Uuid, property_name: &str) -> CalendarRepositoryResult<()>; async fn remove_calendar_property(
&self,
calendar_id: &Uuid,
property_name: &str,
) -> CalendarRepositoryResult<()>;
/// Gets all custom properties for a calendar /// Gets all custom properties for a calendar
async fn get_calendar_properties(&self, calendar_id: &Uuid) -> CalendarRepositoryResult<std::collections::HashMap<String, String>>; async fn get_calendar_properties(
&self,
calendar_id: &Uuid,
) -> CalendarRepositoryResult<std::collections::HashMap<String, String>>;
/// Share calendar with another user /// Share calendar with another user
async fn share_calendar(&self, calendar_id: &Uuid, user_id: &str, access_level: &str) -> CalendarRepositoryResult<()>; async fn share_calendar(
&self,
calendar_id: &Uuid,
user_id: &str,
access_level: &str,
) -> CalendarRepositoryResult<()>;
/// Remove calendar sharing for a user /// Remove calendar sharing for a user
async fn remove_calendar_sharing(&self, calendar_id: &Uuid, user_id: &str) -> CalendarRepositoryResult<()>; async fn remove_calendar_sharing(
&self,
calendar_id: &Uuid,
user_id: &str,
) -> CalendarRepositoryResult<()>;
/// Get calendar sharing information (who has access to this calendar) /// Get calendar sharing information (who has access to this calendar)
async fn get_calendar_shares(&self, calendar_id: &Uuid) -> CalendarRepositoryResult<Vec<(String, String)>>; async fn get_calendar_shares(
} &self,
calendar_id: &Uuid,
) -> CalendarRepositoryResult<Vec<(String, String)>>;
}
+38 -11
View File
@@ -1,6 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use uuid::Uuid;
use std::result::Result; use std::result::Result;
use uuid::Uuid;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use crate::domain::entities::contact::{Contact, ContactGroup}; use crate::domain::entities::contact::{Contact, ContactGroup};
@@ -13,11 +13,23 @@ pub trait ContactRepository: Send + Sync + 'static {
async fn update_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact>; async fn update_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact>;
async fn delete_contact(&self, id: &Uuid) -> ContactRepositoryResult<()>; async fn delete_contact(&self, id: &Uuid) -> ContactRepositoryResult<()>;
async fn get_contact_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<Contact>>; async fn get_contact_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<Contact>>;
async fn get_contact_by_uid(&self, address_book_id: &Uuid, uid: &str) -> ContactRepositoryResult<Option<Contact>>; async fn get_contact_by_uid(
async fn get_contacts_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>>; &self,
address_book_id: &Uuid,
uid: &str,
) -> ContactRepositoryResult<Option<Contact>>;
async fn get_contacts_by_address_book(
&self,
address_book_id: &Uuid,
) -> ContactRepositoryResult<Vec<Contact>>;
async fn get_contacts_by_email(&self, email: &str) -> ContactRepositoryResult<Vec<Contact>>; async fn get_contacts_by_email(&self, email: &str) -> ContactRepositoryResult<Vec<Contact>>;
async fn get_contacts_by_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>>; async fn get_contacts_by_group(&self, group_id: &Uuid)
async fn search_contacts(&self, address_book_id: &Uuid, query: &str) -> ContactRepositoryResult<Vec<Contact>>; -> ContactRepositoryResult<Vec<Contact>>;
async fn search_contacts(
&self,
address_book_id: &Uuid,
query: &str,
) -> ContactRepositoryResult<Vec<Contact>>;
} }
#[async_trait] #[async_trait]
@@ -26,9 +38,24 @@ pub trait ContactGroupRepository: Send + Sync + 'static {
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup>; async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup>;
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()>; async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()>;
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>>; async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>>;
async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>>; async fn get_groups_by_address_book(
async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()>; &self,
async fn remove_contact_from_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()>; address_book_id: &Uuid,
async fn get_contacts_in_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>>; ) -> ContactRepositoryResult<Vec<ContactGroup>>;
async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>>; async fn add_contact_to_group(
} &self,
group_id: &Uuid,
contact_id: &Uuid,
) -> ContactRepositoryResult<()>;
async fn remove_contact_from_group(
&self,
group_id: &Uuid,
contact_id: &Uuid,
) -> ContactRepositoryResult<()>;
async fn get_contacts_in_group(&self, group_id: &Uuid)
-> ContactRepositoryResult<Vec<Contact>>;
async fn get_groups_for_contact(
&self,
contact_id: &Uuid,
) -> ContactRepositoryResult<Vec<ContactGroup>>;
}
+9 -8
View File
@@ -15,9 +15,9 @@ use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use futures::Stream; use futures::Stream;
use crate::common::errors::DomainError;
use crate::domain::entities::file::File; use crate::domain::entities::file::File;
use crate::domain::services::path_service::StoragePath; use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError;
// ───────────────────────────────────────────────────── // ─────────────────────────────────────────────────────
// FileReadRepository — read/query operations // FileReadRepository — read/query operations
@@ -98,17 +98,14 @@ pub trait FileWriteRepository: Send + Sync + 'static {
) -> Result<File, DomainError>; ) -> Result<File, DomainError>;
/// Renames a file (same folder, different name). /// Renames a file (same folder, different name).
async fn rename_file( async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<File, DomainError>;
&self,
file_id: &str,
new_name: &str,
) -> Result<File, DomainError>;
/// Deletes a file. /// Deletes a file.
async fn delete_file(&self, id: &str) -> Result<(), DomainError>; async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
/// Updates the content of an existing file. /// Updates the content of an existing file.
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError>; async fn update_file_content(&self, file_id: &str, content: Vec<u8>)
-> Result<(), DomainError>;
/// Registers file metadata WITHOUT writing content to disk (write-behind). /// Registers file metadata WITHOUT writing content to disk (write-behind).
/// ///
@@ -128,7 +125,11 @@ pub trait FileWriteRepository: Send + Sync + 'static {
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>; async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
/// Restores a file from the trash to its original location /// Restores a file from the trash to its original location
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>; async fn restore_from_trash(
&self,
file_id: &str,
original_path: &str,
) -> Result<(), DomainError>;
/// Permanently deletes a file (used by the trash) /// Permanently deletes a file (used by the trash)
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>; async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
+27 -15
View File
@@ -10,9 +10,9 @@
use async_trait::async_trait; use async_trait::async_trait;
use crate::common::errors::DomainError;
use crate::domain::entities::folder::Folder; use crate::domain::entities::folder::Folder;
use crate::domain::services::path_service::StoragePath; use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError;
/// Domain port for folder persistence. /// Domain port for folder persistence.
/// ///
@@ -21,38 +21,46 @@ use crate::common::errors::DomainError;
#[async_trait] #[async_trait]
pub trait FolderRepository: Send + Sync + 'static { pub trait FolderRepository: Send + Sync + 'static {
/// Creates a new folder /// Creates a new folder
async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError>; async fn create_folder(
&self,
name: String,
parent_id: Option<String>,
) -> Result<Folder, DomainError>;
/// Gets a folder by its ID /// Gets a folder by its ID
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>; async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>;
/// Gets a folder by its storage path /// Gets a folder by its storage path
async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError>; async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError>;
/// Lists folders within a parent folder /// Lists folders within a parent folder
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError>; async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError>;
/// Lists folders with pagination /// Lists folders with pagination
async fn list_folders_paginated( async fn list_folders_paginated(
&self, &self,
parent_id: Option<&str>, parent_id: Option<&str>,
offset: usize, offset: usize,
limit: usize, limit: usize,
include_total: bool include_total: bool,
) -> Result<(Vec<Folder>, Option<usize>), DomainError>; ) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
/// Renames a folder /// Renames a folder
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>; async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
/// Moves a folder to another parent /// Moves a folder to another parent
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> Result<Folder, DomainError>; async fn move_folder(
&self,
id: &str,
new_parent_id: Option<&str>,
) -> Result<Folder, DomainError>;
/// Deletes a folder /// Deletes a folder
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>; async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
/// Checks if a folder exists at the given path /// Checks if a folder exists at the given path
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>; async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
/// Gets the path of a folder /// Gets the path of a folder
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>; async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>;
@@ -62,7 +70,11 @@ pub trait FolderRepository: Send + Sync + 'static {
async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError>; async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError>;
/// Restores a folder from the trash to its original location /// Restores a folder from the trash to its original location
async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> Result<(), DomainError>; async fn restore_from_trash(
&self,
folder_id: &str,
original_path: &str,
) -> Result<(), DomainError>;
/// Permanently deletes a folder (used by the trash) /// Permanently deletes a folder (used by the trash)
async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>; async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>;
+3 -3
View File
@@ -1,11 +1,11 @@
pub mod address_book_repository; pub mod address_book_repository;
pub mod calendar_repository;
pub mod calendar_event_repository; pub mod calendar_event_repository;
pub mod calendar_repository;
pub mod contact_repository; pub mod contact_repository;
pub mod file_repository; pub mod file_repository;
pub mod folder_repository; pub mod folder_repository;
pub mod session_repository; pub mod session_repository;
pub mod settings_repository;
pub mod share_repository; pub mod share_repository;
pub mod trash_repository; pub mod trash_repository;
pub mod settings_repository; pub mod user_repository;
pub mod user_repository;
+20 -20
View File
@@ -1,15 +1,15 @@
use async_trait::async_trait;
use crate::domain::entities::session::Session;
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use crate::domain::entities::session::Session;
use async_trait::async_trait;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum SessionRepositoryError { pub enum SessionRepositoryError {
#[error("Session not found: {0}")] #[error("Session not found: {0}")]
NotFound(String), NotFound(String),
#[error("Database error: {0}")] #[error("Database error: {0}")]
DatabaseError(String), DatabaseError(String),
#[error("Timeout error: {0}")] #[error("Timeout error: {0}")]
Timeout(String), Timeout(String),
} }
@@ -20,15 +20,11 @@ pub type SessionRepositoryResult<T> = Result<T, SessionRepositoryError>;
impl From<SessionRepositoryError> for DomainError { impl From<SessionRepositoryError> for DomainError {
fn from(err: SessionRepositoryError) -> Self { fn from(err: SessionRepositoryError) -> Self {
match err { match err {
SessionRepositoryError::NotFound(msg) => { SessionRepositoryError::NotFound(msg) => DomainError::not_found("Session", msg),
DomainError::not_found("Session", msg)
},
SessionRepositoryError::DatabaseError(msg) => { SessionRepositoryError::DatabaseError(msg) => {
DomainError::internal_error("Database", msg) DomainError::internal_error("Database", msg)
}, }
SessionRepositoryError::Timeout(msg) => { SessionRepositoryError::Timeout(msg) => DomainError::timeout("Database", msg),
DomainError::timeout("Database", msg)
},
} }
} }
} }
@@ -37,22 +33,26 @@ impl From<SessionRepositoryError> for DomainError {
pub trait SessionRepository: Send + Sync + 'static { pub trait SessionRepository: Send + Sync + 'static {
/// Creates a new session /// Creates a new session
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>; async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>;
/// Gets a session by ID /// Gets a session by ID
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session>; async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session>;
/// Gets a session by refresh token /// Gets a session by refresh token
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult<Session>; async fn get_session_by_refresh_token(
&self,
refresh_token: &str,
) -> SessionRepositoryResult<Session>;
/// Gets all sessions for a user /// Gets all sessions for a user
async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>>; async fn get_sessions_by_user_id(&self, user_id: &str)
-> SessionRepositoryResult<Vec<Session>>;
/// Revokes a specific session /// Revokes a specific session
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>; async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>;
/// Revokes all sessions for a user /// Revokes all sessions for a user
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64>; async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64>;
/// Deletes expired sessions /// Deletes expired sessions
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>; async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>;
} }
+28 -27
View File
@@ -1,27 +1,28 @@
use std::collections::HashMap; use crate::common::errors::DomainError;
use async_trait::async_trait; use async_trait::async_trait;
use crate::common::errors::DomainError; use std::collections::HashMap;
/// Repository for platform settings stored in the database. /// Repository for platform settings stored in the database.
/// Settings are key-value pairs organized by category (e.g., "oidc", "general"). /// Settings are key-value pairs organized by category (e.g., "oidc", "general").
#[async_trait] #[async_trait]
pub trait SettingsRepository: Send + Sync + 'static { pub trait SettingsRepository: Send + Sync + 'static {
/// Get a single setting value by key /// Get a single setting value by key
async fn get(&self, key: &str) -> Result<Option<String>, DomainError>; async fn get(&self, key: &str) -> Result<Option<String>, DomainError>;
/// Get all settings for a given category /// Get all settings for a given category
async fn get_by_category(&self, category: &str) -> Result<HashMap<String, String>, DomainError>; async fn get_by_category(&self, category: &str)
-> Result<HashMap<String, String>, DomainError>;
/// Set a setting value (upsert)
async fn set( /// Set a setting value (upsert)
&self, async fn set(
key: &str, &self,
value: &str, key: &str,
category: &str, value: &str,
is_secret: bool, category: &str,
updated_by: Option<&str>, is_secret: bool,
) -> Result<(), DomainError>; updated_by: Option<&str>,
) -> Result<(), DomainError>;
/// Delete a setting by key
async fn delete(&self, key: &str) -> Result<(), DomainError>; /// Delete a setting by key
} async fn delete(&self, key: &str) -> Result<(), DomainError>;
}
+11 -8
View File
@@ -1,4 +1,3 @@
use async_trait::async_trait; use async_trait::async_trait;
use thiserror::Error; use thiserror::Error;
@@ -25,22 +24,26 @@ pub enum ShareRepositoryError {
pub trait ShareRepository: Send + Sync + 'static { pub trait ShareRepository: Send + Sync + 'static {
/// Save a new share or update an existing one /// Save a new share or update an existing one
async fn save(&self, share: &Share) -> Result<Share, ShareRepositoryError>; async fn save(&self, share: &Share) -> Result<Share, ShareRepositoryError>;
/// Find a share by its ID /// Find a share by its ID
async fn find_by_id(&self, id: &str) -> Result<Share, ShareRepositoryError>; async fn find_by_id(&self, id: &str) -> Result<Share, ShareRepositoryError>;
/// Find a share by its token /// Find a share by its token
async fn find_by_token(&self, token: &str) -> Result<Share, ShareRepositoryError>; async fn find_by_token(&self, token: &str) -> Result<Share, ShareRepositoryError>;
/// Find all shares for a specific item /// Find all shares for a specific item
async fn find_by_item(&self, item_id: &str, item_type: &ShareItemType) -> Result<Vec<Share>, ShareRepositoryError>; async fn find_by_item(
&self,
item_id: &str,
item_type: &ShareItemType,
) -> Result<Vec<Share>, ShareRepositoryError>;
/// Delete a share by its ID /// Delete a share by its ID
async fn delete(&self, id: &str) -> Result<(), ShareRepositoryError>; async fn delete(&self, id: &str) -> Result<(), ShareRepositoryError>;
/// Find all shares created by a specific user /// Find all shares created by a specific user
async fn find_by_user(&self, user_id: &str) -> Result<Vec<Share>, ShareRepositoryError>; async fn find_by_user(&self, user_id: &str) -> Result<Vec<Share>, ShareRepositoryError>;
/// Find all shares (admin operation) /// Find all shares (admin operation)
async fn find_all(&self) -> Result<Vec<Share>, ShareRepositoryError>; async fn find_all(&self) -> Result<Vec<Share>, ShareRepositoryError>;
} }
+2 -2
View File
@@ -1,8 +1,8 @@
use async_trait::async_trait; use async_trait::async_trait;
use uuid::Uuid; use uuid::Uuid;
use crate::domain::entities::trashed_item::TrashedItem;
use crate::common::errors::Result; use crate::common::errors::Result;
use crate::domain::entities::trashed_item::TrashedItem;
#[async_trait] #[async_trait]
pub trait TrashRepository: Send + Sync { pub trait TrashRepository: Send + Sync {
@@ -13,4 +13,4 @@ pub trait TrashRepository: Send + Sync {
async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()>; async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()>;
async fn clear_trash(&self, user_id: &Uuid) -> Result<()>; async fn clear_trash(&self, user_id: &Uuid) -> Result<()>;
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>>; async fn get_expired_items(&self) -> Result<Vec<TrashedItem>>;
} }
+45 -41
View File
@@ -1,24 +1,24 @@
use async_trait::async_trait;
use crate::domain::entities::user::{User, UserRole};
use crate::common::errors::DomainError; use crate::common::errors::DomainError;
use crate::domain::entities::user::{User, UserRole};
use async_trait::async_trait;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum UserRepositoryError { pub enum UserRepositoryError {
#[error("User not found: {0}")] #[error("User not found: {0}")]
NotFound(String), NotFound(String),
#[error("User already exists: {0}")] #[error("User already exists: {0}")]
AlreadyExists(String), AlreadyExists(String),
#[error("Database error: {0}")] #[error("Database error: {0}")]
DatabaseError(String), DatabaseError(String),
#[error("Validation error: {0}")] #[error("Validation error: {0}")]
ValidationError(String), ValidationError(String),
#[error("Timeout error: {0}")] #[error("Timeout error: {0}")]
Timeout(String), Timeout(String),
#[error("Operation not allowed: {0}")] #[error("Operation not allowed: {0}")]
OperationNotAllowed(String), OperationNotAllowed(String),
} }
@@ -29,24 +29,14 @@ pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>;
impl From<UserRepositoryError> for DomainError { impl From<UserRepositoryError> for DomainError {
fn from(err: UserRepositoryError) -> Self { fn from(err: UserRepositoryError) -> Self {
match err { match err {
UserRepositoryError::NotFound(msg) => { UserRepositoryError::NotFound(msg) => DomainError::not_found("User", msg),
DomainError::not_found("User", msg) UserRepositoryError::AlreadyExists(msg) => DomainError::already_exists("User", msg),
}, UserRepositoryError::DatabaseError(msg) => DomainError::internal_error("Database", msg),
UserRepositoryError::AlreadyExists(msg) => { UserRepositoryError::ValidationError(msg) => DomainError::validation_error(msg),
DomainError::already_exists("User", msg) UserRepositoryError::Timeout(msg) => DomainError::timeout("Database", msg),
},
UserRepositoryError::DatabaseError(msg) => {
DomainError::internal_error("Database", msg)
},
UserRepositoryError::ValidationError(msg) => {
DomainError::validation_error(msg)
},
UserRepositoryError::Timeout(msg) => {
DomainError::timeout("Database", msg)
},
UserRepositoryError::OperationNotAllowed(msg) => { UserRepositoryError::OperationNotAllowed(msg) => {
DomainError::access_denied("User", msg) DomainError::access_denied("User", msg)
}, }
} }
} }
} }
@@ -55,48 +45,62 @@ impl From<UserRepositoryError> for DomainError {
pub trait UserRepository: Send + Sync + 'static { pub trait UserRepository: Send + Sync + 'static {
/// Creates a new user /// Creates a new user
async fn create_user(&self, user: User) -> UserRepositoryResult<User>; async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
/// Gets a user by ID /// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User>; async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User>;
/// Gets a user by username /// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>; async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>;
/// Gets a user by email /// Gets a user by email
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User>; async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User>;
/// Updates an existing user /// Updates an existing user
async fn update_user(&self, user: User) -> UserRepositoryResult<User>; async fn update_user(&self, user: User) -> UserRepositoryResult<User>;
/// Updates only a user's storage usage /// Updates only a user's storage usage
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()>; async fn update_storage_usage(
&self,
user_id: &str,
usage_bytes: i64,
) -> UserRepositoryResult<()>;
/// Updates the last login date /// Updates the last login date
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()>; async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()>;
/// Lists users with pagination /// Lists users with pagination
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>>; async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>>;
/// Activates or deactivates a user /// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()>; async fn set_user_active_status(&self, user_id: &str, active: bool)
-> UserRepositoryResult<()>;
/// Changes a user's password /// Changes a user's password
async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()>; async fn change_password(&self, user_id: &str, password_hash: &str)
-> UserRepositoryResult<()>;
/// Changes a user's role /// Changes a user's role
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()>; async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()>;
/// Lists users by role (admin or user) /// Lists users by role (admin or user)
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>; async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
/// Deletes a user /// Deletes a user
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>; async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>;
/// Finds a user by OIDC provider + subject pair /// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> UserRepositoryResult<User>; async fn get_user_by_oidc_subject(
&self,
provider: &str,
subject: &str,
) -> UserRepositoryResult<User>;
/// Updates a user's storage quota /// Updates a user's storage quota
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()>; async fn update_storage_quota(
&self,
user_id: &str,
quota_bytes: i64,
) -> UserRepositoryResult<()>;
/// Counts the total number of users /// Counts the total number of users
async fn count_users(&self) -> UserRepositoryResult<i64>; async fn count_users(&self) -> UserRepositoryResult<i64>;
@@ -114,4 +118,4 @@ pub struct StorageStats {
pub total_used_bytes: i64, pub total_used_bytes: i64,
pub users_over_80_percent: i64, pub users_over_80_percent: i64,
pub users_over_quota: i64, pub users_over_quota: i64,
} }
+8 -8
View File
@@ -6,10 +6,10 @@ use thiserror::Error;
pub enum I18nError { pub enum I18nError {
#[error("Translation key not found: {0}")] #[error("Translation key not found: {0}")]
KeyNotFound(String), KeyNotFound(String),
#[error("Invalid locale: {0}")] #[error("Invalid locale: {0}")]
InvalidLocale(String), InvalidLocale(String),
#[error("Error loading translations: {0}")] #[error("Error loading translations: {0}")]
LoadError(String), LoadError(String),
} }
@@ -38,7 +38,7 @@ impl Locale {
Locale::Portuguese => "pt", Locale::Portuguese => "pt",
} }
} }
/// Create from locale code string /// Create from locale code string
pub fn from_str(code: &str) -> Option<Self> { pub fn from_str(code: &str) -> Option<Self> {
match code.to_lowercase().as_str() { match code.to_lowercase().as_str() {
@@ -50,7 +50,7 @@ impl Locale {
_ => None, _ => None,
} }
} }
/// Get default locale /// Get default locale
pub fn default() -> Self { pub fn default() -> Self {
Locale::English Locale::English
@@ -62,13 +62,13 @@ impl Locale {
pub trait I18nService: Send + Sync + 'static { pub trait I18nService: Send + Sync + 'static {
/// Get a translation for a key and locale /// Get a translation for a key and locale
async fn translate(&self, key: &str, locale: Locale) -> I18nResult<String>; async fn translate(&self, key: &str, locale: Locale) -> I18nResult<String>;
/// Load translations for a locale /// Load translations for a locale
async fn load_translations(&self, locale: Locale) -> I18nResult<()>; async fn load_translations(&self, locale: Locale) -> I18nResult<()>;
/// Get available locales /// Get available locales
async fn available_locales(&self) -> Vec<Locale>; async fn available_locales(&self) -> Vec<Locale>;
/// Check if a locale is supported /// Check if a locale is supported
async fn is_supported(&self, locale: Locale) -> bool; async fn is_supported(&self, locale: Locale) -> bool;
} }
+1 -1
View File
@@ -2,4 +2,4 @@ pub mod i18n_service;
pub mod path_service; pub mod path_service;
// NOTE: auth_service has been moved to infrastructure/services/jwt_service.rs // NOTE: auth_service has been moved to infrastructure/services/jwt_service.rs
// The functionality is now exposed through application/ports/auth_ports.rs (TokenServicePort) // The functionality is now exposed through application/ports/auth_ports.rs (TokenServicePort)
+27 -21
View File
@@ -1,7 +1,7 @@
//! StoragePath - Domain Value Object for representing storage paths //! StoragePath - Domain Value Object for representing storage paths
//! //!
//! This module contains only the StoragePath Value Object which is part of the pure domain. //! This module contains only the StoragePath Value Object which is part of the pure domain.
//! PathService (which implements StoragePort and StorageMediator) was moved to //! PathService (which implements StoragePort and StorageMediator) was moved to
//! infrastructure/services/path_service.rs because it has file system dependencies. //! infrastructure/services/path_service.rs because it has file system dependencies.
use std::path::PathBuf; use std::path::PathBuf;
@@ -17,12 +17,14 @@ impl StoragePath {
pub fn new(segments: Vec<String>) -> Self { pub fn new(segments: Vec<String>) -> Self {
Self { segments } Self { segments }
} }
/// Creates an empty path (root) /// Creates an empty path (root)
pub fn root() -> Self { pub fn root() -> Self {
Self { segments: Vec::new() } Self {
segments: Vec::new(),
}
} }
/// Creates a path from a string with segments separated by / /// Creates a path from a string with segments separated by /
pub fn from_string(path: &str) -> Self { pub fn from_string(path: &str) -> Self {
let segments = path let segments = path
@@ -32,7 +34,7 @@ impl StoragePath {
.collect(); .collect();
Self { segments } Self { segments }
} }
/// Creates a path from a PathBuf /// Creates a path from a PathBuf
pub fn from(path_buf: PathBuf) -> Self { pub fn from(path_buf: PathBuf) -> Self {
let segments = path_buf let segments = path_buf
@@ -44,34 +46,38 @@ impl StoragePath {
.collect(); .collect();
Self { segments } Self { segments }
} }
/// Appends a segment to the path /// Appends a segment to the path
pub fn join(&self, segment: &str) -> Self { pub fn join(&self, segment: &str) -> Self {
let mut new_segments = self.segments.clone(); let mut new_segments = self.segments.clone();
new_segments.push(segment.to_string()); new_segments.push(segment.to_string());
Self { segments: new_segments } Self {
segments: new_segments,
}
} }
/// Gets the file name (last segment) /// Gets the file name (last segment)
pub fn file_name(&self) -> Option<String> { pub fn file_name(&self) -> Option<String> {
self.segments.last().cloned() self.segments.last().cloned()
} }
/// Gets the parent directory path /// Gets the parent directory path
pub fn parent(&self) -> Option<Self> { pub fn parent(&self) -> Option<Self> {
if self.segments.is_empty() { if self.segments.is_empty() {
None None
} else { } else {
let parent_segments = self.segments[..self.segments.len() - 1].to_vec(); let parent_segments = self.segments[..self.segments.len() - 1].to_vec();
Some(Self { segments: parent_segments }) Some(Self {
segments: parent_segments,
})
} }
} }
/// Checks if the path is empty (is the root) /// Checks if the path is empty (is the root)
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.segments.is_empty() self.segments.is_empty()
} }
/// Converts the path to a string with format "/segment1/segment2/..." /// Converts the path to a string with format "/segment1/segment2/..."
pub fn to_string(&self) -> String { pub fn to_string(&self) -> String {
if self.segments.is_empty() { if self.segments.is_empty() {
@@ -80,7 +86,7 @@ impl StoragePath {
format!("/{}", self.segments.join("/")) format!("/{}", self.segments.join("/"))
} }
} }
/// Returns the path representation as a string /// Returns the path representation as a string
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
// Note: The implementation should really store the string, // Note: The implementation should really store the string,
@@ -88,7 +94,7 @@ impl StoragePath {
// This is only used for the get_folder_path_str implementation // This is only used for the get_folder_path_str implementation
"/" "/"
} }
/// Gets the path segments /// Gets the path segments
pub fn segments(&self) -> &[String] { pub fn segments(&self) -> &[String] {
&self.segments &self.segments
@@ -98,38 +104,38 @@ impl StoragePath {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_storage_path_from_string() { fn test_storage_path_from_string() {
let path = StoragePath::from_string("folder/subfolder/file.txt"); let path = StoragePath::from_string("folder/subfolder/file.txt");
assert_eq!(path.segments(), &["folder", "subfolder", "file.txt"]); assert_eq!(path.segments(), &["folder", "subfolder", "file.txt"]);
assert_eq!(path.to_string(), "/folder/subfolder/file.txt"); assert_eq!(path.to_string(), "/folder/subfolder/file.txt");
} }
#[test] #[test]
fn test_storage_path_join() { fn test_storage_path_join() {
let path = StoragePath::from_string("folder"); let path = StoragePath::from_string("folder");
let joined = path.join("file.txt"); let joined = path.join("file.txt");
assert_eq!(joined.to_string(), "/folder/file.txt"); assert_eq!(joined.to_string(), "/folder/file.txt");
} }
#[test] #[test]
fn test_storage_path_parent() { fn test_storage_path_parent() {
let path = StoragePath::from_string("folder/file.txt"); let path = StoragePath::from_string("folder/file.txt");
let parent = path.parent().unwrap(); let parent = path.parent().unwrap();
assert_eq!(parent.to_string(), "/folder"); assert_eq!(parent.to_string(), "/folder");
} }
#[test] #[test]
fn test_storage_path_root() { fn test_storage_path_root() {
let root = StoragePath::root(); let root = StoragePath::root();
assert!(root.is_empty()); assert!(root.is_empty());
assert_eq!(root.to_string(), "/"); assert_eq!(root.to_string(), "/");
} }
#[test] #[test]
fn test_storage_path_file_name() { fn test_storage_path_file_name() {
let path = StoragePath::from_string("folder/file.txt"); let path = StoragePath::from_string("folder/file.txt");
assert_eq!(path.file_name(), Some("file.txt".to_string())); assert_eq!(path.file_name(), Some("file.txt".to_string()));
} }
} }
@@ -1,298 +1,467 @@
//! Calendar Storage Adapter //! Calendar Storage Adapter
//! //!
//! This adapter implements the `CalendarStoragePort` application port using //! This adapter implements the `CalendarStoragePort` application port using
//! the `CalendarRepository` and `CalendarEventRepository` domain repositories. //! the `CalendarRepository` and `CalendarEventRepository` domain repositories.
//! It bridges the gap between the application layer and the infrastructure layer. //! It bridges the gap between the application layer and the infrastructure layer.
use std::sync::Arc; use async_trait::async_trait;
use std::collections::HashMap; use chrono::{DateTime, Utc};
use async_trait::async_trait; use std::collections::HashMap;
use chrono::{DateTime, Utc}; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
use crate::application::dtos::calendar_dto::{ use crate::application::dtos::calendar_dto::{
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto, CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto,
CreateEventDto, UpdateEventDto, CreateEventICalDto UpdateCalendarDto, UpdateEventDto,
}; };
use crate::application::ports::calendar_ports::CalendarStoragePort; use crate::application::ports::calendar_ports::CalendarStoragePort;
use crate::common::errors::{DomainError, ErrorKind}; use crate::common::errors::{DomainError, ErrorKind};
use crate::domain::entities::calendar::Calendar; use crate::domain::entities::calendar::Calendar;
use crate::domain::entities::calendar_event::CalendarEvent; use crate::domain::entities::calendar_event::CalendarEvent;
use crate::domain::repositories::calendar_repository::CalendarRepository; use crate::domain::repositories::calendar_event_repository::CalendarEventRepository;
use crate::domain::repositories::calendar_event_repository::CalendarEventRepository; use crate::domain::repositories::calendar_repository::CalendarRepository;
/// Adapter that implements CalendarStoragePort using domain repositories /// Adapter that implements CalendarStoragePort using domain repositories
pub struct CalendarStorageAdapter { pub struct CalendarStorageAdapter {
calendar_repository: Arc<dyn CalendarRepository>, calendar_repository: Arc<dyn CalendarRepository>,
event_repository: Arc<dyn CalendarEventRepository>, event_repository: Arc<dyn CalendarEventRepository>,
} }
impl CalendarStorageAdapter { impl CalendarStorageAdapter {
/// Creates a new CalendarStorageAdapter with the given repositories /// Creates a new CalendarStorageAdapter with the given repositories
pub fn new( pub fn new(
calendar_repository: Arc<dyn CalendarRepository>, calendar_repository: Arc<dyn CalendarRepository>,
event_repository: Arc<dyn CalendarEventRepository>, event_repository: Arc<dyn CalendarEventRepository>,
) -> Self { ) -> Self {
Self { Self {
calendar_repository, calendar_repository,
event_repository, event_repository,
} }
} }
} }
#[async_trait] #[async_trait]
impl CalendarStoragePort for CalendarStorageAdapter { impl CalendarStoragePort for CalendarStorageAdapter {
// Calendar operations // Calendar operations
async fn create_calendar(&self, dto: CreateCalendarDto, owner_id: &str) -> Result<CalendarDto, DomainError> { async fn create_calendar(
let calendar = Calendar::new( &self,
dto.name, dto: CreateCalendarDto,
owner_id.to_string(), owner_id: &str,
dto.description, ) -> Result<CalendarDto, DomainError> {
dto.color, let calendar = Calendar::new(dto.name, owner_id.to_string(), dto.description, dto.color)?;
)?;
let created = self.calendar_repository.create_calendar(calendar).await?;
let created = self.calendar_repository.create_calendar(calendar).await?; Ok(CalendarDto::from(created))
Ok(CalendarDto::from(created)) }
}
async fn update_calendar(
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError> { &self,
let uuid = Uuid::parse_str(calendar_id) calendar_id: &str,
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; update: UpdateCalendarDto,
) -> Result<CalendarDto, DomainError> {
let mut calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?; let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
DomainError::new(
if let Some(name) = update.name { ErrorKind::InvalidInput,
calendar.update_name(name)?; "Calendar",
} "Invalid calendar ID format",
if let Some(description) = update.description { )
calendar.update_description(Some(description)); })?;
}
if let Some(color) = update.color { let mut calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?;
calendar.update_color(Some(color))?;
} if let Some(name) = update.name {
calendar.update_name(name)?;
let updated = self.calendar_repository.update_calendar(calendar).await?; }
Ok(CalendarDto::from(updated)) if let Some(description) = update.description {
} calendar.update_description(Some(description));
}
async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError> { if let Some(color) = update.color {
let uuid = Uuid::parse_str(calendar_id) calendar.update_color(Some(color))?;
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; }
// First delete all events in the calendar let updated = self.calendar_repository.update_calendar(calendar).await?;
self.event_repository.delete_all_events_in_calendar(&uuid).await?; Ok(CalendarDto::from(updated))
}
// Then delete the calendar itself
self.calendar_repository.delete_calendar(&uuid).await async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError> {
} let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
DomainError::new(
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError> { ErrorKind::InvalidInput,
let uuid = Uuid::parse_str(calendar_id) "Calendar",
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; "Invalid calendar ID format",
)
let calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?; })?;
Ok(CalendarDto::from(calendar))
} // First delete all events in the calendar
self.event_repository
async fn list_calendars_by_owner(&self, owner_id: &str) -> Result<Vec<CalendarDto>, DomainError> { .delete_all_events_in_calendar(&uuid)
let calendars = self.calendar_repository.list_calendars_by_owner(owner_id).await?; .await?;
Ok(calendars.into_iter().map(CalendarDto::from).collect())
} // Then delete the calendar itself
self.calendar_repository.delete_calendar(&uuid).await
async fn list_calendars_shared_with_user(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError> { }
let calendars = self.calendar_repository.list_calendars_shared_with_user(user_id).await?;
Ok(calendars.into_iter().map(CalendarDto::from).collect()) async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError> {
} let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
DomainError::new(
async fn list_public_calendars(&self, limit: i64, offset: i64) -> Result<Vec<CalendarDto>, DomainError> { ErrorKind::InvalidInput,
let calendars = self.calendar_repository.list_public_calendars(limit, offset).await?; "Calendar",
Ok(calendars.into_iter().map(CalendarDto::from).collect()) "Invalid calendar ID format",
} )
})?;
async fn check_calendar_access(&self, calendar_id: &str, user_id: &str) -> Result<bool, DomainError> {
let uuid = Uuid::parse_str(calendar_id) let calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?;
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; Ok(CalendarDto::from(calendar))
}
self.calendar_repository.user_has_calendar_access(&uuid, user_id).await
} async fn list_calendars_by_owner(
&self,
// Calendar sharing owner_id: &str,
) -> Result<Vec<CalendarDto>, DomainError> {
async fn share_calendar(&self, calendar_id: &str, user_id: &str, access_level: &str) -> Result<(), DomainError> { let calendars = self
let uuid = Uuid::parse_str(calendar_id) .calendar_repository
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; .list_calendars_by_owner(owner_id)
.await?;
self.calendar_repository.share_calendar(&uuid, user_id, access_level).await Ok(calendars.into_iter().map(CalendarDto::from).collect())
} }
async fn remove_calendar_sharing(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError> { async fn list_calendars_shared_with_user(
let uuid = Uuid::parse_str(calendar_id) &self,
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; user_id: &str,
) -> Result<Vec<CalendarDto>, DomainError> {
self.calendar_repository.remove_calendar_sharing(&uuid, user_id).await let calendars = self
} .calendar_repository
.list_calendars_shared_with_user(user_id)
async fn get_calendar_shares(&self, calendar_id: &str) -> Result<Vec<(String, String)>, DomainError> { .await?;
let uuid = Uuid::parse_str(calendar_id) Ok(calendars.into_iter().map(CalendarDto::from).collect())
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; }
self.calendar_repository.get_calendar_shares(&uuid).await async fn list_public_calendars(
} &self,
limit: i64,
// Calendar properties offset: i64,
) -> Result<Vec<CalendarDto>, DomainError> {
async fn set_calendar_property(&self, calendar_id: &str, property_name: &str, property_value: &str) -> Result<(), DomainError> { let calendars = self
let uuid = Uuid::parse_str(calendar_id) .calendar_repository
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; .list_public_calendars(limit, offset)
.await?;
self.calendar_repository.set_calendar_property(&uuid, property_name, property_value).await Ok(calendars.into_iter().map(CalendarDto::from).collect())
} }
async fn get_calendar_property(&self, calendar_id: &str, property_name: &str) -> Result<Option<String>, DomainError> { async fn check_calendar_access(
let uuid = Uuid::parse_str(calendar_id) &self,
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; calendar_id: &str,
user_id: &str,
self.calendar_repository.get_calendar_property(&uuid, property_name).await ) -> Result<bool, DomainError> {
} let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
DomainError::new(
async fn get_calendar_properties(&self, calendar_id: &str) -> Result<HashMap<String, String>, DomainError> { ErrorKind::InvalidInput,
let uuid = Uuid::parse_str(calendar_id) "Calendar",
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; "Invalid calendar ID format",
)
self.calendar_repository.get_calendar_properties(&uuid).await })?;
}
self.calendar_repository
// Event operations .user_has_calendar_access(&uuid, user_id)
.await
async fn create_event(&self, dto: CreateEventDto) -> Result<CalendarEventDto, DomainError> { }
let calendar_id = Uuid::parse_str(&dto.calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid calendar ID format"))?; // Calendar sharing
// Verify calendar exists and user has access async fn share_calendar(
let _calendar = self.calendar_repository.find_calendar_by_id(&calendar_id).await?; &self,
calendar_id: &str,
// Generate basic iCal data user_id: &str,
let ical_data = format!( access_level: &str,
"BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//OxiCloud//EN\nBEGIN:VEVENT\nUID:{}@oxicloud\nDTSTAMP:{}\nDTSTART:{}\nDTEND:{}\nSUMMARY:{}\nEND:VEVENT\nEND:VCALENDAR", ) -> Result<(), DomainError> {
uuid::Uuid::new_v4(), let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
chrono::Utc::now().format("%Y%m%dT%H%M%SZ"), DomainError::new(
dto.start_time.format("%Y%m%dT%H%M%SZ"), ErrorKind::InvalidInput,
dto.end_time.format("%Y%m%dT%H%M%SZ"), "Calendar",
dto.summary "Invalid calendar ID format",
); )
})?;
let event = CalendarEvent::new(
calendar_id, self.calendar_repository
dto.summary, .share_calendar(&uuid, user_id, access_level)
dto.description, .await
dto.location, }
dto.start_time,
dto.end_time, async fn remove_calendar_sharing(
dto.all_day.unwrap_or(false), &self,
dto.rrule, calendar_id: &str,
ical_data, user_id: &str,
)?; ) -> Result<(), DomainError> {
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
let created = self.event_repository.create_event(event).await?; DomainError::new(
Ok(CalendarEventDto::from(created)) ErrorKind::InvalidInput,
} "Calendar",
"Invalid calendar ID format",
async fn create_event_from_ical(&self, dto: CreateEventICalDto) -> Result<CalendarEventDto, DomainError> { )
let calendar_id = Uuid::parse_str(&dto.calendar_id) })?;
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid calendar ID format"))?;
self.calendar_repository
// Verify calendar exists .remove_calendar_sharing(&uuid, user_id)
let _calendar = self.calendar_repository.find_calendar_by_id(&calendar_id).await?; .await
}
// Parse iCal data and create event
let event = CalendarEvent::from_ical(calendar_id, dto.ical_data.clone())?; async fn get_calendar_shares(
&self,
let created = self.event_repository.create_event(event).await?; calendar_id: &str,
Ok(CalendarEventDto::from(created)) ) -> Result<Vec<(String, String)>, DomainError> {
} let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
DomainError::new(
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError> { ErrorKind::InvalidInput,
let uuid = Uuid::parse_str(event_id) "Calendar",
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?; "Invalid calendar ID format",
)
let mut event = self.event_repository.find_event_by_id(&uuid).await?; })?;
if let Some(summary) = update.summary { self.calendar_repository.get_calendar_shares(&uuid).await
event.update_summary(summary)?; }
}
if let Some(description) = update.description { // Calendar properties
event.update_description(Some(description));
} async fn set_calendar_property(
if let Some(location) = update.location { &self,
event.update_location(Some(location)); calendar_id: &str,
} property_name: &str,
if let Some(start_time) = update.start_time { property_value: &str,
if let Some(end_time) = update.end_time { ) -> Result<(), DomainError> {
event.update_time_range(start_time, end_time)?; let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
} else { DomainError::new(
event.update_time_range(start_time, *event.end_time())?; ErrorKind::InvalidInput,
} "Calendar",
} else if let Some(end_time) = update.end_time { "Invalid calendar ID format",
event.update_time_range(*event.start_time(), end_time)?; )
} })?;
if let Some(all_day) = update.all_day {
event.update_all_day(all_day); self.calendar_repository
} .set_calendar_property(&uuid, property_name, property_value)
if let Some(rrule) = update.rrule { .await
event.update_rrule(Some(rrule))?; }
}
async fn get_calendar_property(
let updated = self.event_repository.update_event(event).await?; &self,
Ok(CalendarEventDto::from(updated)) calendar_id: &str,
} property_name: &str,
) -> Result<Option<String>, DomainError> {
async fn delete_event(&self, event_id: &str) -> Result<(), DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
let uuid = Uuid::parse_str(event_id) DomainError::new(
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?; ErrorKind::InvalidInput,
"Calendar",
self.event_repository.delete_event(&uuid).await "Invalid calendar ID format",
} )
})?;
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError> {
let uuid = Uuid::parse_str(event_id) self.calendar_repository
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?; .get_calendar_property(&uuid, property_name)
.await
let event = self.event_repository.find_event_by_id(&uuid).await?; }
Ok(CalendarEventDto::from(event))
} async fn get_calendar_properties(
&self,
async fn list_events_by_calendar(&self, calendar_id: &str) -> Result<Vec<CalendarEventDto>, DomainError> { calendar_id: &str,
let uuid = Uuid::parse_str(calendar_id) ) -> Result<HashMap<String, String>, DomainError> {
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
DomainError::new(
let events = self.event_repository.list_events_by_calendar(&uuid).await?; ErrorKind::InvalidInput,
Ok(events.into_iter().map(CalendarEventDto::from).collect()) "Calendar",
} "Invalid calendar ID format",
)
async fn list_events_by_calendar_paginated(&self, calendar_id: &str, limit: i64, offset: i64) -> Result<Vec<CalendarEventDto>, DomainError> { })?;
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; self.calendar_repository
.get_calendar_properties(&uuid)
let events = self.event_repository.list_events_by_calendar_paginated(&uuid, limit, offset).await?; .await
Ok(events.into_iter().map(CalendarEventDto::from).collect()) }
}
// Event operations
async fn get_events_in_time_range(
&self, async fn create_event(&self, dto: CreateEventDto) -> Result<CalendarEventDto, DomainError> {
calendar_id: &str, let calendar_id = Uuid::parse_str(&dto.calendar_id).map_err(|_| {
start: &DateTime<Utc>, DomainError::new(
end: &DateTime<Utc> ErrorKind::InvalidInput,
) -> Result<Vec<CalendarEventDto>, DomainError> { "Event",
let uuid = Uuid::parse_str(calendar_id) "Invalid calendar ID format",
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; )
})?;
let events = self.event_repository.get_events_in_time_range(&uuid, start, end).await?;
Ok(events.into_iter().map(CalendarEventDto::from).collect()) // Verify calendar exists and user has access
} let _calendar = self
} .calendar_repository
.find_calendar_by_id(&calendar_id)
#[cfg(test)] .await?;
mod tests {
// Tests would go here using mock repositories // Generate basic iCal data
} let ical_data = format!(
"BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//OxiCloud//EN\nBEGIN:VEVENT\nUID:{}@oxicloud\nDTSTAMP:{}\nDTSTART:{}\nDTEND:{}\nSUMMARY:{}\nEND:VEVENT\nEND:VCALENDAR",
uuid::Uuid::new_v4(),
chrono::Utc::now().format("%Y%m%dT%H%M%SZ"),
dto.start_time.format("%Y%m%dT%H%M%SZ"),
dto.end_time.format("%Y%m%dT%H%M%SZ"),
dto.summary
);
let event = CalendarEvent::new(
calendar_id,
dto.summary,
dto.description,
dto.location,
dto.start_time,
dto.end_time,
dto.all_day.unwrap_or(false),
dto.rrule,
ical_data,
)?;
let created = self.event_repository.create_event(event).await?;
Ok(CalendarEventDto::from(created))
}
async fn create_event_from_ical(
&self,
dto: CreateEventICalDto,
) -> Result<CalendarEventDto, DomainError> {
let calendar_id = Uuid::parse_str(&dto.calendar_id).map_err(|_| {
DomainError::new(
ErrorKind::InvalidInput,
"Event",
"Invalid calendar ID format",
)
})?;
// Verify calendar exists
let _calendar = self
.calendar_repository
.find_calendar_by_id(&calendar_id)
.await?;
// Parse iCal data and create event
let event = CalendarEvent::from_ical(calendar_id, dto.ical_data.clone())?;
let created = self.event_repository.create_event(event).await?;
Ok(CalendarEventDto::from(created))
}
async fn update_event(
&self,
event_id: &str,
update: UpdateEventDto,
) -> Result<CalendarEventDto, DomainError> {
let uuid = Uuid::parse_str(event_id).map_err(|_| {
DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format")
})?;
let mut event = self.event_repository.find_event_by_id(&uuid).await?;
if let Some(summary) = update.summary {
event.update_summary(summary)?;
}
if let Some(description) = update.description {
event.update_description(Some(description));
}
if let Some(location) = update.location {
event.update_location(Some(location));
}
if let Some(start_time) = update.start_time {
if let Some(end_time) = update.end_time {
event.update_time_range(start_time, end_time)?;
} else {
event.update_time_range(start_time, *event.end_time())?;
}
} else if let Some(end_time) = update.end_time {
event.update_time_range(*event.start_time(), end_time)?;
}
if let Some(all_day) = update.all_day {
event.update_all_day(all_day);
}
if let Some(rrule) = update.rrule {
event.update_rrule(Some(rrule))?;
}
let updated = self.event_repository.update_event(event).await?;
Ok(CalendarEventDto::from(updated))
}
async fn delete_event(&self, event_id: &str) -> Result<(), DomainError> {
let uuid = Uuid::parse_str(event_id).map_err(|_| {
DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format")
})?;
self.event_repository.delete_event(&uuid).await
}
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError> {
let uuid = Uuid::parse_str(event_id).map_err(|_| {
DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format")
})?;
let event = self.event_repository.find_event_by_id(&uuid).await?;
Ok(CalendarEventDto::from(event))
}
async fn list_events_by_calendar(
&self,
calendar_id: &str,
) -> Result<Vec<CalendarEventDto>, DomainError> {
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
DomainError::new(
ErrorKind::InvalidInput,
"Calendar",
"Invalid calendar ID format",
)
})?;
let events = self.event_repository.list_events_by_calendar(&uuid).await?;
Ok(events.into_iter().map(CalendarEventDto::from).collect())
}
async fn list_events_by_calendar_paginated(
&self,
calendar_id: &str,
limit: i64,
offset: i64,
) -> Result<Vec<CalendarEventDto>, DomainError> {
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
DomainError::new(
ErrorKind::InvalidInput,
"Calendar",
"Invalid calendar ID format",
)
})?;
let events = self
.event_repository
.list_events_by_calendar_paginated(&uuid, limit, offset)
.await?;
Ok(events.into_iter().map(CalendarEventDto::from).collect())
}
async fn get_events_in_time_range(
&self,
calendar_id: &str,
start: &DateTime<Utc>,
end: &DateTime<Utc>,
) -> Result<Vec<CalendarEventDto>, DomainError> {
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
DomainError::new(
ErrorKind::InvalidInput,
"Calendar",
"Invalid calendar ID format",
)
})?;
let events = self
.event_repository
.get_events_in_time_range(&uuid, start, end)
.await?;
Ok(events.into_iter().map(CalendarEventDto::from).collect())
}
}
#[cfg(test)]
mod tests {
// Tests would go here using mock repositories
}
File diff suppressed because it is too large Load Diff
+128 -125
View File
@@ -1,125 +1,128 @@
//! Infrastructure Error Adapters //! Infrastructure Error Adapters
//! //!
//! This module contains error conversion adapters for infrastructure-specific errors. //! This module contains error conversion adapters for infrastructure-specific errors.
//! These adapters bridge the gap between infrastructure errors (sqlx, serde_json, etc.) //! These adapters bridge the gap between infrastructure errors (sqlx, serde_json, etc.)
//! and domain errors, keeping the domain layer clean of infrastructure knowledge. //! and domain errors, keeping the domain layer clean of infrastructure knowledge.
//! //!
//! Following Clean Architecture principles, these conversions are placed in the //! Following Clean Architecture principles, these conversions are placed in the
//! infrastructure layer rather than the common/domain layers. //! infrastructure layer rather than the common/domain layers.
use crate::domain::errors::{DomainError, ErrorKind}; use crate::domain::errors::{DomainError, ErrorKind};
/// Macro to create From implementations for infrastructure errors to DomainError. /// Macro to create From implementations for infrastructure errors to DomainError.
/// ///
/// This macro is intended for use ONLY within the infrastructure layer. /// This macro is intended for use ONLY within the infrastructure layer.
/// The domain layer should not depend on specific infrastructure error types. /// The domain layer should not depend on specific infrastructure error types.
/// ///
/// # Example /// # Example
/// ///
/// ```ignore /// ```ignore
/// // In infrastructure code: /// // In infrastructure code:
/// impl_infra_error_to_domain!(serde_json::Error, "Serialization"); /// impl_infra_error_to_domain!(serde_json::Error, "Serialization");
/// impl_infra_error_to_domain!(sqlx::Error, "Database"); /// impl_infra_error_to_domain!(sqlx::Error, "Database");
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! impl_infra_error_to_domain { macro_rules! impl_infra_error_to_domain {
($error_type:ty, $entity_type:expr) => { ($error_type:ty, $entity_type:expr) => {
impl From<$error_type> for $crate::domain::errors::DomainError { impl From<$error_type> for $crate::domain::errors::DomainError {
fn from(err: $error_type) -> Self { fn from(err: $error_type) -> Self {
$crate::domain::errors::DomainError { $crate::domain::errors::DomainError {
kind: $crate::domain::errors::ErrorKind::InternalError, kind: $crate::domain::errors::ErrorKind::InternalError,
entity_type: $entity_type, entity_type: $entity_type,
entity_id: None, entity_id: None,
message: format!("{}", err), message: format!("{}", err),
source: Some(Box::new(err)), source: Some(Box::new(err)),
} }
} }
} }
}; };
} }
// Note: We intentionally DO NOT create global From implementations for sqlx::Error // Note: We intentionally DO NOT create global From implementations for sqlx::Error
// or serde_json::Error here. Each repository/service should handle its own error // or serde_json::Error here. Each repository/service should handle its own error
// conversions with proper context. This prevents the domain from depending on // conversions with proper context. This prevents the domain from depending on
// infrastructure error types. // infrastructure error types.
/// Helper trait for converting infrastructure errors to DomainError with context. /// Helper trait for converting infrastructure errors to DomainError with context.
/// ///
/// This trait provides a more explicit way to convert infrastructure errors /// This trait provides a more explicit way to convert infrastructure errors
/// to domain errors, requiring the caller to provide context about the entity /// to domain errors, requiring the caller to provide context about the entity
/// being operated on. /// being operated on.
pub trait IntoDomainError { pub trait IntoDomainError {
/// Convert the error to a DomainError with the given entity type context. /// Convert the error to a DomainError with the given entity type context.
fn into_domain_error(self, entity_type: &'static str) -> DomainError; fn into_domain_error(self, entity_type: &'static str) -> DomainError;
} }
impl IntoDomainError for std::io::Error { impl IntoDomainError for std::io::Error {
fn into_domain_error(self, entity_type: &'static str) -> DomainError { fn into_domain_error(self, entity_type: &'static str) -> DomainError {
DomainError::new( DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
entity_type, entity_type,
format!("IO error: {}", self), format!("IO error: {}", self),
).with_source(self) )
} .with_source(self)
} }
}
impl IntoDomainError for serde_json::Error {
fn into_domain_error(self, entity_type: &'static str) -> DomainError { impl IntoDomainError for serde_json::Error {
DomainError::new( fn into_domain_error(self, entity_type: &'static str) -> DomainError {
ErrorKind::InternalError, DomainError::new(
entity_type, ErrorKind::InternalError,
format!("Serialization error: {}", self), entity_type,
).with_source(self) format!("Serialization error: {}", self),
} )
} .with_source(self)
}
impl IntoDomainError for sqlx::Error { }
fn into_domain_error(self, entity_type: &'static str) -> DomainError {
match &self { impl IntoDomainError for sqlx::Error {
sqlx::Error::RowNotFound => { fn into_domain_error(self, entity_type: &'static str) -> DomainError {
DomainError::not_found(entity_type, "Record not found") match &self {
} sqlx::Error::RowNotFound => DomainError::not_found(entity_type, "Record not found"),
sqlx::Error::Database(db_err) => { sqlx::Error::Database(db_err) => {
// Handle specific PostgreSQL error codes // Handle specific PostgreSQL error codes
if db_err.code().is_some_and(|c| c == "23505") { if db_err.code().is_some_and(|c| c == "23505") {
DomainError::already_exists(entity_type, "Record already exists") DomainError::already_exists(entity_type, "Record already exists")
} else { } else {
DomainError::new( DomainError::new(
ErrorKind::DatabaseError, ErrorKind::DatabaseError,
entity_type, entity_type,
format!("Database error: {}", db_err), format!("Database error: {}", db_err),
).with_source(self) )
} .with_source(self)
} }
_ => DomainError::new( }
ErrorKind::InternalError, _ => DomainError::new(
entity_type, ErrorKind::InternalError,
format!("Database error: {}", self), entity_type,
).with_source(self) format!("Database error: {}", self),
} )
} .with_source(self),
} }
}
#[cfg(test)] }
mod tests {
use super::*; #[cfg(test)]
mod tests {
#[test] use super::*;
fn test_io_error_conversion() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); #[test]
let domain_error = io_error.into_domain_error("File"); fn test_io_error_conversion() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
assert_eq!(domain_error.entity_type, "File"); let domain_error = io_error.into_domain_error("File");
assert!(domain_error.message.contains("IO error"));
} assert_eq!(domain_error.entity_type, "File");
assert!(domain_error.message.contains("IO error"));
#[test] }
fn test_serde_json_error_conversion() {
let json_str = "{ invalid json }"; #[test]
let serde_error: serde_json::Error = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err(); fn test_serde_json_error_conversion() {
let domain_error = serde_error.into_domain_error("Config"); let json_str = "{ invalid json }";
let serde_error: serde_json::Error =
assert_eq!(domain_error.entity_type, "Config"); serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
assert!(domain_error.message.contains("Serialization error")); let domain_error = serde_error.into_domain_error("Config");
}
} assert_eq!(domain_error.entity_type, "Config");
assert!(domain_error.message.contains("Serialization error"));
}
}
+16 -16
View File
@@ -1,16 +1,16 @@
//! Infrastructure Adapters //! Infrastructure Adapters
//! //!
//! This module contains adapters that bridge the gap between domain repositories //! This module contains adapters that bridge the gap between domain repositories
//! and application ports. These adapters implement the application layer ports //! and application ports. These adapters implement the application layer ports
//! using the infrastructure layer repositories. //! using the infrastructure layer repositories.
//! //!
//! It also includes error adapters for converting infrastructure-specific errors //! It also includes error adapters for converting infrastructure-specific errors
//! to domain errors, following Clean Architecture principles. //! to domain errors, following Clean Architecture principles.
pub mod calendar_storage_adapter; pub mod calendar_storage_adapter;
pub mod contact_storage_adapter; pub mod contact_storage_adapter;
pub mod error_adapters; pub mod error_adapters;
pub use calendar_storage_adapter::CalendarStorageAdapter; pub use calendar_storage_adapter::CalendarStorageAdapter;
pub use contact_storage_adapter::ContactStorageAdapter; pub use contact_storage_adapter::ContactStorageAdapter;
pub use error_adapters::IntoDomainError; pub use error_adapters::IntoDomainError;
+20 -17
View File
@@ -1,21 +1,21 @@
use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use sqlx::PgPool; use sqlx::PgPool;
use std::sync::Arc;
use crate::application::ports::auth_ports::TokenServicePort; use crate::application::ports::auth_ports::TokenServicePort;
use crate::application::services::auth_application_service::AuthApplicationService; use crate::application::services::auth_application_service::AuthApplicationService;
use crate::application::services::folder_service::FolderService; use crate::application::services::folder_service::FolderService;
use crate::infrastructure::repositories::{UserPgRepository, SessionPgRepository};
use crate::infrastructure::services::password_hasher::Argon2PasswordHasher;
use crate::infrastructure::services::jwt_service::JwtTokenService;
use crate::infrastructure::services::oidc_service::OidcService;
use crate::common::config::AppConfig; use crate::common::config::AppConfig;
use crate::common::di::AuthServices; use crate::common::di::AuthServices;
use crate::infrastructure::repositories::{SessionPgRepository, UserPgRepository};
use crate::infrastructure::services::jwt_service::JwtTokenService;
use crate::infrastructure::services::oidc_service::OidcService;
use crate::infrastructure::services::password_hasher::Argon2PasswordHasher;
pub async fn create_auth_services( pub async fn create_auth_services(
config: &AppConfig, config: &AppConfig,
pool: Arc<PgPool>, pool: Arc<PgPool>,
folder_service: Option<Arc<FolderService>> folder_service: Option<Arc<FolderService>>,
) -> Result<AuthServices> { ) -> Result<AuthServices> {
// Create JWT token service (TokenServicePort implementation) // Create JWT token service (TokenServicePort implementation)
let token_service: Arc<dyn TokenServicePort> = Arc::new(JwtTokenService::new( let token_service: Arc<dyn TokenServicePort> = Arc::new(JwtTokenService::new(
@@ -23,14 +23,14 @@ pub async fn create_auth_services(
config.auth.access_token_expiry_secs, config.auth.access_token_expiry_secs,
config.auth.refresh_token_expiry_secs, config.auth.refresh_token_expiry_secs,
)); ));
// Create password hashing service // Create password hashing service
let password_hasher = Arc::new(Argon2PasswordHasher::new()); let password_hasher = Arc::new(Argon2PasswordHasher::new());
// Create PostgreSQL repositories // Create PostgreSQL repositories
let user_repository = Arc::new(UserPgRepository::new(pool.clone())); let user_repository = Arc::new(UserPgRepository::new(pool.clone()));
let session_repository = Arc::new(SessionPgRepository::new(pool.clone())); let session_repository = Arc::new(SessionPgRepository::new(pool.clone()));
// Create authentication application service // Create authentication application service
let mut auth_app_service = AuthApplicationService::new( let mut auth_app_service = AuthApplicationService::new(
user_repository, user_repository,
@@ -39,7 +39,7 @@ pub async fn create_auth_services(
token_service.clone(), token_service.clone(),
config.storage_path.clone(), config.storage_path.clone(),
); );
// Configure folder service if available // Configure folder service if available
if let Some(folder_svc) = folder_service { if let Some(folder_svc) = folder_service {
auth_app_service = auth_app_service.with_folder_service(folder_svc); auth_app_service = auth_app_service.with_folder_service(folder_svc);
@@ -47,9 +47,12 @@ pub async fn create_auth_services(
// Configure OIDC service if enabled // Configure OIDC service if enabled
if config.oidc.enabled { if config.oidc.enabled {
tracing::info!("Initializing OIDC service (provider: {}, issuer: {})", tracing::info!(
config.oidc.provider_name, config.oidc.issuer_url); "Initializing OIDC service (provider: {}, issuer: {})",
config.oidc.provider_name,
config.oidc.issuer_url
);
let oidc_service = Arc::new(OidcService::new(config.oidc.clone())); let oidc_service = Arc::new(OidcService::new(config.oidc.clone()));
auth_app_service = auth_app_service.with_oidc(oidc_service, config.oidc.clone()); auth_app_service = auth_app_service.with_oidc(oidc_service, config.oidc.clone());
@@ -57,12 +60,12 @@ pub async fn create_auth_services(
tracing::warn!("Password login is DISABLED — only OIDC authentication is allowed"); tracing::warn!("Password login is DISABLED — only OIDC authentication is allowed");
} }
} }
// Package service in Arc // Package service in Arc
let auth_application_service = Arc::new(auth_app_service); let auth_application_service = Arc::new(auth_app_service);
Ok(AuthServices { Ok(AuthServices {
token_service, token_service,
auth_application_service, auth_application_service,
}) })
} }

Some files were not shown because too many files have changed in this diff Show More