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:
@@ -1,16 +1,20 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use quick_xml::{
|
||||
Reader, Writer,
|
||||
events::{BytesEnd, BytesStart, BytesText, Event},
|
||||
};
|
||||
/**
|
||||
* CalDAV Adapter Module
|
||||
*
|
||||
* This module provides conversion between CalDAV protocol XML structures and OxiCloud domain objects.
|
||||
* It handles parsing CalDAV request XML and generating CalDAV response XML according to RFC 4791.
|
||||
*/
|
||||
|
||||
use std::io::{Read, Write, BufReader};
|
||||
use chrono::{DateTime, Utc};
|
||||
use quick_xml::{Reader, Writer, events::{Event, BytesStart, BytesEnd, BytesText}};
|
||||
use std::io::{BufReader, Read, Write};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::adapters::webdav_adapter::{WebDavAdapter, QualifiedName, PropFindType, PropFindRequest, Result, WebDavError};
|
||||
use crate::application::adapters::webdav_adapter::{
|
||||
PropFindRequest, PropFindType, QualifiedName, Result, WebDavAdapter, WebDavError,
|
||||
};
|
||||
use crate::application::dtos::calendar_dto::{CalendarDto, CalendarEventDto};
|
||||
|
||||
/// CalDAV report type
|
||||
@@ -30,7 +34,7 @@ pub enum CalDavReportType {
|
||||
SyncCollection {
|
||||
sync_token: String,
|
||||
props: Vec<QualifiedName>,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/// CalDAV adapter for converting between XML and domain objects
|
||||
@@ -61,16 +65,23 @@ impl CalDavAdapter {
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
match name_str {
|
||||
s if s == "calendar-query" || s.ends_with(":calendar-query") => in_calendar_query = true,
|
||||
s if s == "calendar-multiget" || s.ends_with(":calendar-multiget") => in_calendar_multiget = true,
|
||||
s if s == "sync-collection" || s.ends_with(":sync-collection") => in_sync_collection = true,
|
||||
s if s == "calendar-query" || s.ends_with(":calendar-query") => {
|
||||
in_calendar_query = true
|
||||
}
|
||||
s if s == "calendar-multiget" || s.ends_with(":calendar-multiget") => {
|
||||
in_calendar_multiget = true
|
||||
}
|
||||
s if s == "sync-collection" || s.ends_with(":sync-collection") => {
|
||||
in_sync_collection = true
|
||||
}
|
||||
s if s == "prop" || s.ends_with(":prop") => in_prop = true,
|
||||
s if s == "filter" || s.ends_with(":filter") => in_filter = true,
|
||||
s if s == "time-range" || s.ends_with(":time-range") => {
|
||||
// Parse time-range attributes
|
||||
for attr in e.attributes() {
|
||||
if let Ok(attr) = attr {
|
||||
let attr_name = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
|
||||
let attr_name =
|
||||
std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
|
||||
let attr_value = attr.unescape_value().unwrap_or_default();
|
||||
|
||||
if attr_name == "start" {
|
||||
@@ -85,23 +96,23 @@ impl CalDavAdapter {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
s if s == "sync-token" || s.ends_with(":sync-token") => {
|
||||
// We'll capture the text in the Text event
|
||||
},
|
||||
}
|
||||
s if s == "href" || s.ends_with(":href") => {
|
||||
// We'll capture the text in the Text event
|
||||
},
|
||||
}
|
||||
_ if in_prop => {
|
||||
// Add property to request
|
||||
let namespace = WebDavAdapter::extract_namespace(name_str);
|
||||
let prop_name = WebDavAdapter::extract_local_name(name_str);
|
||||
|
||||
props.push(QualifiedName::new(namespace, prop_name));
|
||||
},
|
||||
}
|
||||
_ => { /* Ignore other elements */ }
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::Text(e)) => {
|
||||
let text = e.decode().unwrap_or_default();
|
||||
|
||||
@@ -114,7 +125,7 @@ impl CalDavAdapter {
|
||||
if (in_calendar_multiget || in_sync_collection) && !in_prop && !in_filter {
|
||||
hrefs.push(text.to_string());
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::End(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
@@ -123,10 +134,11 @@ impl CalDavAdapter {
|
||||
// Don't reset report-type flags — they're needed at EOF for decision logic
|
||||
s if s == "prop" || s.ends_with(":prop") => in_prop = false,
|
||||
s if s == "filter" || s.ends_with(":filter") => in_filter = false,
|
||||
s if s == "time-range" || s.ends_with(":time-range") => { /* time-range end, attributes already parsed */ },
|
||||
_ => ()
|
||||
s if s == "time-range" || s.ends_with(":time-range") => { /* time-range end, attributes already parsed */
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Event::Empty(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
@@ -141,7 +153,8 @@ impl CalDavAdapter {
|
||||
// Parse time-range attributes
|
||||
for attr in e.attributes() {
|
||||
if let Ok(attr) = attr {
|
||||
let attr_name = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
|
||||
let attr_name =
|
||||
std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
|
||||
let attr_value = attr.unescape_value().unwrap_or_default();
|
||||
|
||||
if attr_name == "start" {
|
||||
@@ -157,7 +170,7 @@ impl CalDavAdapter {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(WebDavError::XmlError(e)),
|
||||
_ => (),
|
||||
@@ -175,20 +188,11 @@ impl CalDavAdapter {
|
||||
None
|
||||
};
|
||||
|
||||
CalDavReportType::CalendarQuery {
|
||||
time_range,
|
||||
props,
|
||||
}
|
||||
CalDavReportType::CalendarQuery { time_range, props }
|
||||
} else if in_calendar_multiget {
|
||||
CalDavReportType::CalendarMultiget {
|
||||
hrefs,
|
||||
props,
|
||||
}
|
||||
CalDavReportType::CalendarMultiget { hrefs, props }
|
||||
} else if in_sync_collection {
|
||||
CalDavReportType::SyncCollection {
|
||||
sync_token,
|
||||
props,
|
||||
}
|
||||
CalDavReportType::SyncCollection { sync_token, props }
|
||||
} else {
|
||||
// Default to empty calendar query
|
||||
CalDavReportType::CalendarQuery {
|
||||
@@ -210,15 +214,22 @@ impl CalDavAdapter {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
// Start multistatus response
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:multistatus").with_attributes([
|
||||
xml_writer.write_event(Event::Start(
|
||||
BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
("xmlns:C", "urn:ietf:params:xml:ns:caldav"),
|
||||
("xmlns:CS", "http://calendarserver.org/ns/"),
|
||||
])))?;
|
||||
]),
|
||||
))?;
|
||||
|
||||
// Add responses for calendars
|
||||
for calendar in calendars {
|
||||
Self::write_calendar_response(&mut xml_writer, calendar, request, &format!("{}{}/", base_href, calendar.id))?;
|
||||
Self::write_calendar_response(
|
||||
&mut xml_writer,
|
||||
calendar,
|
||||
request,
|
||||
&format!("{}{}/", base_href, calendar.id),
|
||||
)?;
|
||||
}
|
||||
|
||||
// End multistatus
|
||||
@@ -253,11 +264,11 @@ impl CalDavAdapter {
|
||||
PropFindType::AllProp => {
|
||||
// Write all standard properties for a calendar
|
||||
Self::write_calendar_standard_props(xml_writer, calendar)?;
|
||||
},
|
||||
}
|
||||
PropFindType::PropName => {
|
||||
// Write only property names (empty elements)
|
||||
Self::write_calendar_prop_names(xml_writer)?;
|
||||
},
|
||||
}
|
||||
PropFindType::Prop(props) => {
|
||||
// Write requested properties
|
||||
Self::write_calendar_requested_props(xml_writer, calendar, props)?;
|
||||
@@ -301,7 +312,9 @@ impl CalDavAdapter {
|
||||
|
||||
// Last modified
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&calendar.updated_at.to_rfc2822())))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(
|
||||
&calendar.updated_at.to_rfc2822(),
|
||||
)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
|
||||
// ETag
|
||||
@@ -311,15 +324,23 @@ impl CalDavAdapter {
|
||||
|
||||
// Content type for calendar collection
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("text/calendar; component=VCALENDAR")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(
|
||||
"text/calendar; component=VCALENDAR",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
|
||||
// CalDAV specific properties
|
||||
|
||||
// Supported calendar component set
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("C:supported-calendar-component-set")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:comp").with_attributes([("name", "VEVENT")])))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("C:supported-calendar-component-set")))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new(
|
||||
"C:supported-calendar-component-set",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::Empty(
|
||||
BytesStart::new("C:comp").with_attributes([("name", "VEVENT")]),
|
||||
))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new(
|
||||
"C:supported-calendar-component-set",
|
||||
)))?;
|
||||
|
||||
// Calendar timezone (empty for UTC)
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-timezone")))?;
|
||||
@@ -335,13 +356,16 @@ impl CalDavAdapter {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-access")))?;
|
||||
|
||||
// Current user privilege set
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:current-user-privilege-set")))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new(
|
||||
"D:current-user-privilege-set",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:read")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:privilege")))?;
|
||||
|
||||
// Only add write privilege if user owns the calendar or has write access
|
||||
if calendar.owner_id == "current_user_id" { // This should be replaced with actual user check
|
||||
if calendar.owner_id == "current_user_id" {
|
||||
// This should be replaced with actual user check
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:write")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:privilege")))?;
|
||||
@@ -370,9 +394,7 @@ impl CalDavAdapter {
|
||||
}
|
||||
|
||||
/// Write calendar property names
|
||||
fn write_calendar_prop_names<W: Write>(
|
||||
xml_writer: &mut Writer<W>,
|
||||
) -> Result<()> {
|
||||
fn write_calendar_prop_names<W: Write>(xml_writer: &mut Writer<W>) -> Result<()> {
|
||||
// Common WebDAV property names
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:displayname")))?;
|
||||
@@ -381,11 +403,15 @@ impl CalDavAdapter {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:getcontenttype")))?;
|
||||
|
||||
// CalDAV specific property names
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:supported-calendar-component-set")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(
|
||||
"C:supported-calendar-component-set",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-timezone")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("CS:calendar-color")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-access")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:current-user-privilege-set")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(
|
||||
"D:current-user-privilege-set",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-description")))?;
|
||||
|
||||
Ok(())
|
||||
@@ -405,75 +431,97 @@ impl CalDavAdapter {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:collection")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:resourcetype")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "displayname") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:displayname")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&calendar.name)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:displayname")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getlastmodified") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&calendar.updated_at.to_rfc2822())))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(
|
||||
&calendar.updated_at.to_rfc2822(),
|
||||
)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getetag") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", calendar.id))))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!(
|
||||
"\"{}\"",
|
||||
calendar.id
|
||||
))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getcontenttype") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("text/calendar; component=VCALENDAR")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(
|
||||
"text/calendar; component=VCALENDAR",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "current-user-privilege-set") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:current-user-privilege-set")))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new(
|
||||
"D:current-user-privilege-set",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:read")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:privilege")))?;
|
||||
|
||||
// Only add write privilege if user owns the calendar or has write access
|
||||
if calendar.owner_id == "current_user_id" { // This should be replaced with actual user check
|
||||
if calendar.owner_id == "current_user_id" {
|
||||
// This should be replaced with actual user check
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:write")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:privilege")))?;
|
||||
}
|
||||
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:current-user-privilege-set")))?;
|
||||
},
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("D:current-user-privilege-set")))?;
|
||||
}
|
||||
|
||||
// CalDAV namespace properties
|
||||
("urn:ietf:params:xml:ns:caldav", "supported-calendar-component-set") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("C:supported-calendar-component-set")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:comp").with_attributes([("name", "VEVENT")])))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("C:supported-calendar-component-set")))?;
|
||||
},
|
||||
xml_writer.write_event(Event::Start(BytesStart::new(
|
||||
"C:supported-calendar-component-set",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::Empty(
|
||||
BytesStart::new("C:comp").with_attributes([("name", "VEVENT")]),
|
||||
))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new(
|
||||
"C:supported-calendar-component-set",
|
||||
)))?;
|
||||
}
|
||||
("urn:ietf:params:xml:ns:caldav", "calendar-timezone") => {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-timezone")))?;
|
||||
},
|
||||
}
|
||||
("urn:ietf:params:xml:ns:caldav", "calendar-access") => {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-access")))?;
|
||||
},
|
||||
}
|
||||
("urn:ietf:params:xml:ns:caldav", "calendar-description") => {
|
||||
if let Some(desc) = &calendar.description {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("C:calendar-description")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("C:calendar-description")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(desc)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("C:calendar-description")))?;
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("C:calendar-description")))?;
|
||||
} else {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-description")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Empty(BytesStart::new("C:calendar-description")))?;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// CalendarServer namespace properties
|
||||
("http://calendarserver.org/ns/", "calendar-color") => {
|
||||
if let Some(color) = &calendar.color {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("CS:calendar-color")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("CS:calendar-color")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(color)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("CS:calendar-color")))?;
|
||||
} else {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("CS:calendar-color")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Empty(BytesStart::new("CS:calendar-color")))?;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Custom properties from the calendar
|
||||
_ => {
|
||||
@@ -524,11 +572,13 @@ impl CalDavAdapter {
|
||||
) -> Result<()> {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:multistatus").with_attributes([
|
||||
xml_writer.write_event(Event::Start(
|
||||
BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
("xmlns:C", "urn:ietf:params:xml:ns:caldav"),
|
||||
("xmlns:CS", "http://calendarserver.org/ns/"),
|
||||
])))?;
|
||||
]),
|
||||
))?;
|
||||
|
||||
// Write the calendar collection itself
|
||||
Self::write_calendar_response(&mut xml_writer, calendar, request, base_href)?;
|
||||
@@ -552,17 +602,21 @@ impl CalDavAdapter {
|
||||
|
||||
// getetag
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", event.id))))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&format!("\"{}\"", event.id))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
|
||||
// getcontenttype
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("text/calendar; component=vevent")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(
|
||||
"text/calendar; component=vevent",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
|
||||
// getlastmodified
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&event.updated_at.to_rfc2822())))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&event.updated_at.to_rfc2822())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
|
||||
@@ -590,11 +644,13 @@ impl CalDavAdapter {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
// Start multistatus response
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:multistatus").with_attributes([
|
||||
xml_writer.write_event(Event::Start(
|
||||
BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
("xmlns:C", "urn:ietf:params:xml:ns:caldav"),
|
||||
("xmlns:CS", "http://calendarserver.org/ns/"),
|
||||
])))?;
|
||||
]),
|
||||
))?;
|
||||
|
||||
// Determine which properties to include based on request type
|
||||
let props = match request {
|
||||
@@ -681,7 +737,9 @@ impl CalDavAdapter {
|
||||
|
||||
// Content type
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("text/calendar; component=VEVENT")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(
|
||||
"text/calendar; component=VEVENT",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
|
||||
// Last modified
|
||||
@@ -712,7 +770,10 @@ impl CalDavAdapter {
|
||||
event.summary.replace("\n", "\\n"),
|
||||
event.start_time.format("%Y%m%dT%H%M%SZ"),
|
||||
event.end_time.format("%Y%m%dT%H%M%SZ"),
|
||||
event.rrule.as_ref().map_or("".to_string(), |r| format!("RRULE:{}\r\n", r)),
|
||||
event
|
||||
.rrule
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |r| format!("RRULE:{}\r\n", r)),
|
||||
event.updated_at.format("%Y%m%dT%H%M%SZ"),
|
||||
);
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&ical_data)))?;
|
||||
@@ -732,22 +793,26 @@ impl CalDavAdapter {
|
||||
// DAV namespace properties
|
||||
("DAV:", "resourcetype") => {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getetag") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", event.id))))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&format!("\"{}\"", event.id))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getcontenttype") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("text/calendar; component=VEVENT")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(
|
||||
"text/calendar; component=VEVENT",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getlastmodified") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&event.updated_at.to_rfc2822())))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&event.updated_at.to_rfc2822())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
},
|
||||
}
|
||||
|
||||
// CalDAV namespace properties
|
||||
("urn:ietf:params:xml:ns:caldav", "calendar-data") => {
|
||||
@@ -771,12 +836,15 @@ impl CalDavAdapter {
|
||||
event.summary.replace("\n", "\\n"),
|
||||
event.start_time.format("%Y%m%dT%H%M%SZ"),
|
||||
event.end_time.format("%Y%m%dT%H%M%SZ"),
|
||||
event.rrule.as_ref().map_or("".to_string(), |r| format!("RRULE:{}\r\n", r)),
|
||||
event
|
||||
.rrule
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |r| format!("RRULE:{}\r\n", r)),
|
||||
event.updated_at.format("%Y%m%dT%H%M%SZ"),
|
||||
);
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&ical_data)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("C:calendar-data")))?;
|
||||
},
|
||||
}
|
||||
|
||||
// Property not supported
|
||||
_ => {
|
||||
@@ -800,7 +868,9 @@ impl CalDavAdapter {
|
||||
}
|
||||
|
||||
/// Parse a MKCALENDAR XML request
|
||||
pub fn parse_mkcalendar<R: Read>(reader: R) -> Result<(String, Option<String>, Option<String>)> {
|
||||
pub fn parse_mkcalendar<R: Read>(
|
||||
reader: R,
|
||||
) -> Result<(String, Option<String>, Option<String>)> {
|
||||
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
|
||||
xml_reader.config_mut().trim_text(true);
|
||||
|
||||
@@ -823,15 +893,28 @@ impl CalDavAdapter {
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
match name_str {
|
||||
s if s == "mkcalendar" || s.ends_with(":mkcalendar") => in_mkcalendar = true,
|
||||
s if s == "mkcalendar" || s.ends_with(":mkcalendar") => {
|
||||
in_mkcalendar = true
|
||||
}
|
||||
s if in_mkcalendar && (s == "set" || s.ends_with(":set")) => in_set = true,
|
||||
s if in_set && (s == "prop" || s.ends_with(":prop")) => in_prop = true,
|
||||
s if in_prop && (s == "displayname" || s.ends_with(":displayname")) => in_displayname = true,
|
||||
s if in_prop && (s == "calendar-description" || s.ends_with(":calendar-description")) => in_description = true,
|
||||
s if in_prop && (s == "calendar-color" || s.ends_with(":calendar-color")) => in_calendar_color = true,
|
||||
_ => ()
|
||||
s if in_prop && (s == "displayname" || s.ends_with(":displayname")) => {
|
||||
in_displayname = true
|
||||
}
|
||||
s if in_prop
|
||||
&& (s == "calendar-description"
|
||||
|| s.ends_with(":calendar-description")) =>
|
||||
{
|
||||
in_description = true
|
||||
}
|
||||
s if in_prop
|
||||
&& (s == "calendar-color" || s.ends_with(":calendar-color")) =>
|
||||
{
|
||||
in_calendar_color = true
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Event::Text(e)) => {
|
||||
let text = e.decode().unwrap_or_default();
|
||||
|
||||
@@ -842,21 +925,31 @@ impl CalDavAdapter {
|
||||
} else if in_calendar_color {
|
||||
color = Some(text.to_string());
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::End(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
match name_str {
|
||||
s if s == "mkcalendar" || s.ends_with(":mkcalendar") => in_mkcalendar = false,
|
||||
s if s == "mkcalendar" || s.ends_with(":mkcalendar") => {
|
||||
in_mkcalendar = false
|
||||
}
|
||||
s if s == "set" || s.ends_with(":set") => in_set = false,
|
||||
s if s == "prop" || s.ends_with(":prop") => in_prop = false,
|
||||
s if s == "displayname" || s.ends_with(":displayname") => in_displayname = false,
|
||||
s if s == "calendar-description" || s.ends_with(":calendar-description") => in_description = false,
|
||||
s if s == "calendar-color" || s.ends_with(":calendar-color") => in_calendar_color = false,
|
||||
_ => ()
|
||||
s if s == "displayname" || s.ends_with(":displayname") => {
|
||||
in_displayname = false
|
||||
}
|
||||
s if s == "calendar-description"
|
||||
|| s.ends_with(":calendar-description") =>
|
||||
{
|
||||
in_description = false
|
||||
}
|
||||
s if s == "calendar-color" || s.ends_with(":calendar-color") => {
|
||||
in_calendar_color = false
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(WebDavError::XmlError(e)),
|
||||
_ => (),
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
use std::collections::HashMap;
|
||||
use chrono::{Utc, TimeZone};
|
||||
use crate::application::adapters::caldav_adapter::{CalDavAdapter, CalDavReportType};
|
||||
use crate::application::adapters::webdav_adapter::{PropFindRequest, PropFindType, QualifiedName};
|
||||
use crate::application::adapters::webdav_adapter::{
|
||||
PropFindRequest, PropFindType, QualifiedName,
|
||||
};
|
||||
use crate::application::dtos::calendar_dto::{CalendarDto, CalendarEventDto};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
|
||||
fn sample_calendar() -> CalendarDto {
|
||||
CalendarDto {
|
||||
@@ -56,7 +58,11 @@ mod tests {
|
||||
</C:mkcalendar>"#;
|
||||
|
||||
let result = CalDavAdapter::parse_mkcalendar(Cursor::new(xml));
|
||||
assert!(result.is_ok(), "Failed to parse MKCALENDAR: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse MKCALENDAR: {:?}",
|
||||
result.err()
|
||||
);
|
||||
let (name, desc, color) = result.unwrap();
|
||||
assert_eq!(name, "Work Calendar");
|
||||
assert_eq!(desc, Some("Work related events".to_string()));
|
||||
@@ -131,7 +137,11 @@ mod tests {
|
||||
</C:calendar-multiget>"#;
|
||||
|
||||
let result = CalDavAdapter::parse_report(Cursor::new(xml));
|
||||
assert!(result.is_ok(), "Failed to parse multiget: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse multiget: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
match result.unwrap() {
|
||||
CalDavReportType::CalendarMultiget { hrefs, props } => {
|
||||
@@ -163,12 +173,25 @@ mod tests {
|
||||
"/caldav/",
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "Failed to generate propfind response: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to generate propfind response: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let xml_str = String::from_utf8(output).expect("Invalid UTF-8 in response");
|
||||
assert!(xml_str.contains("multistatus"), "Response should contain multistatus element");
|
||||
assert!(xml_str.contains("Personal"), "Response should contain calendar name");
|
||||
assert!(xml_str.contains("cal-001"), "Response should contain calendar ID in href");
|
||||
assert!(
|
||||
xml_str.contains("multistatus"),
|
||||
"Response should contain multistatus element"
|
||||
);
|
||||
assert!(
|
||||
xml_str.contains("Personal"),
|
||||
"Response should contain calendar name"
|
||||
);
|
||||
assert!(
|
||||
xml_str.contains("cal-001"),
|
||||
"Response should contain calendar ID in href"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -189,7 +212,11 @@ mod tests {
|
||||
"0",
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "Failed to generate collection propfind: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to generate collection propfind: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
|
||||
assert!(xml_str.contains("multistatus"), "Should have multistatus");
|
||||
@@ -215,13 +242,20 @@ mod tests {
|
||||
"1",
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "Failed to generate depth-1 propfind: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to generate depth-1 propfind: {:?}",
|
||||
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("Personal"), "Should have calendar name");
|
||||
// Depth 1 should include event resources
|
||||
assert!(xml_str.contains("evt-001"), "Depth 1 should include event resources");
|
||||
assert!(
|
||||
xml_str.contains("evt-001"),
|
||||
"Depth 1 should include event resources"
|
||||
);
|
||||
}
|
||||
|
||||
// ========================
|
||||
@@ -253,14 +287,27 @@ mod tests {
|
||||
"/caldav/cal-001",
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "Failed to generate events response: {:?}", result.err());
|
||||
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");
|
||||
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]
|
||||
@@ -279,8 +326,14 @@ mod tests {
|
||||
"/caldav/cal-001",
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "Empty events should still produce valid response");
|
||||
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");
|
||||
assert!(
|
||||
xml_str.contains("multistatus"),
|
||||
"Should have multistatus even for empty"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use quick_xml::{
|
||||
Reader, Writer,
|
||||
events::{BytesEnd, BytesStart, BytesText, Event},
|
||||
};
|
||||
/**
|
||||
* CardDAV Adapter Module
|
||||
*
|
||||
@@ -5,12 +9,10 @@
|
||||
* OxiCloud domain objects. It handles parsing CardDAV request XML and generating
|
||||
* CardDAV response XML according to RFC 6352.
|
||||
*/
|
||||
|
||||
use std::io::{Read, Write, BufReader};
|
||||
use quick_xml::{Reader, Writer, events::{Event, BytesStart, BytesEnd, BytesText}};
|
||||
use std::io::{BufReader, Read, Write};
|
||||
|
||||
use crate::application::adapters::webdav_adapter::{
|
||||
WebDavAdapter, QualifiedName, PropFindType, PropFindRequest, Result, WebDavError,
|
||||
PropFindRequest, PropFindType, QualifiedName, Result, WebDavAdapter, WebDavError,
|
||||
};
|
||||
use crate::application::dtos::address_book_dto::AddressBookDto;
|
||||
use crate::application::dtos::contact_dto::ContactDto;
|
||||
@@ -19,9 +21,7 @@ use crate::application::dtos::contact_dto::ContactDto;
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum CardDavReportType {
|
||||
/// Addressbook-query report
|
||||
AddressbookQuery {
|
||||
props: Vec<QualifiedName>,
|
||||
},
|
||||
AddressbookQuery { props: Vec<QualifiedName> },
|
||||
/// Addressbook-multiget report
|
||||
AddressbookMultiget {
|
||||
hrefs: Vec<String>,
|
||||
@@ -61,20 +61,30 @@ impl CardDavAdapter {
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
match name_str {
|
||||
s if s == "addressbook-query" || s.ends_with(":addressbook-query") => in_addressbook_query = true,
|
||||
s if s == "addressbook-multiget" || s.ends_with(":addressbook-multiget") => in_addressbook_multiget = true,
|
||||
s if s == "sync-collection" || s.ends_with(":sync-collection") => in_sync_collection = true,
|
||||
s if s == "addressbook-query" || s.ends_with(":addressbook-query") => {
|
||||
in_addressbook_query = true
|
||||
}
|
||||
s if s == "addressbook-multiget"
|
||||
|| s.ends_with(":addressbook-multiget") =>
|
||||
{
|
||||
in_addressbook_multiget = true
|
||||
}
|
||||
s if s == "sync-collection" || s.ends_with(":sync-collection") => {
|
||||
in_sync_collection = true
|
||||
}
|
||||
s if s == "prop" || s.ends_with(":prop") => in_prop = true,
|
||||
s if s == "href" || s.ends_with(":href") => in_href = true,
|
||||
s if s == "sync-token" || s.ends_with(":sync-token") => in_sync_token = true,
|
||||
s if s == "sync-token" || s.ends_with(":sync-token") => {
|
||||
in_sync_token = true
|
||||
}
|
||||
_ if in_prop => {
|
||||
let namespace = WebDavAdapter::extract_namespace(name_str);
|
||||
let prop_name = WebDavAdapter::extract_local_name(name_str);
|
||||
props.push(QualifiedName::new(namespace, prop_name));
|
||||
},
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::Text(e)) => {
|
||||
let text = e.decode().unwrap_or_default();
|
||||
if in_href {
|
||||
@@ -82,7 +92,7 @@ impl CardDavAdapter {
|
||||
} else if in_sync_token {
|
||||
sync_token = text.to_string();
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::End(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
@@ -90,17 +100,19 @@ impl CardDavAdapter {
|
||||
match name_str {
|
||||
s if s == "prop" || s.ends_with(":prop") => in_prop = false,
|
||||
s if s == "href" || s.ends_with(":href") => in_href = false,
|
||||
s if s == "sync-token" || s.ends_with(":sync-token") => in_sync_token = false,
|
||||
s if s == "sync-token" || s.ends_with(":sync-token") => {
|
||||
in_sync_token = false
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::Empty(ref e)) if in_prop => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
let namespace = WebDavAdapter::extract_namespace(name_str);
|
||||
let prop_name = WebDavAdapter::extract_local_name(name_str);
|
||||
props.push(QualifiedName::new(namespace, prop_name));
|
||||
},
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(WebDavError::XmlError(e)),
|
||||
_ => (),
|
||||
@@ -129,14 +141,21 @@ impl CardDavAdapter {
|
||||
) -> Result<()> {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:multistatus").with_attributes([
|
||||
xml_writer.write_event(Event::Start(
|
||||
BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
("xmlns:CR", "urn:ietf:params:xml:ns:carddav"),
|
||||
("xmlns:CS", "http://calendarserver.org/ns/"),
|
||||
])))?;
|
||||
]),
|
||||
))?;
|
||||
|
||||
for book in address_books {
|
||||
Self::write_addressbook_response(&mut xml_writer, book, request, &format!("{}{}/", base_href, book.id))?;
|
||||
Self::write_addressbook_response(
|
||||
&mut xml_writer,
|
||||
book,
|
||||
request,
|
||||
&format!("{}{}/", base_href, book.id),
|
||||
)?;
|
||||
}
|
||||
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:multistatus")))?;
|
||||
@@ -154,11 +173,13 @@ impl CardDavAdapter {
|
||||
) -> Result<()> {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:multistatus").with_attributes([
|
||||
xml_writer.write_event(Event::Start(
|
||||
BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
("xmlns:CR", "urn:ietf:params:xml:ns:carddav"),
|
||||
("xmlns:CS", "http://calendarserver.org/ns/"),
|
||||
])))?;
|
||||
]),
|
||||
))?;
|
||||
|
||||
// Write the address book itself
|
||||
Self::write_addressbook_response(&mut xml_writer, address_book, request, base_href)?;
|
||||
@@ -194,7 +215,9 @@ impl CardDavAdapter {
|
||||
match &request.prop_find_type {
|
||||
PropFindType::AllProp => Self::write_addressbook_all_props(xml_writer, book)?,
|
||||
PropFindType::PropName => Self::write_addressbook_prop_names(xml_writer)?,
|
||||
PropFindType::Prop(props) => Self::write_addressbook_requested_props(xml_writer, book, props)?,
|
||||
PropFindType::Prop(props) => {
|
||||
Self::write_addressbook_requested_props(xml_writer, book, props)?
|
||||
}
|
||||
}
|
||||
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
|
||||
@@ -241,14 +264,14 @@ impl CardDavAdapter {
|
||||
|
||||
// supported-address-data
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("CR:supported-address-data")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("CR:address-data-type").with_attributes([
|
||||
("content-type", "text/vcard"),
|
||||
("version", "3.0"),
|
||||
])))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("CR:address-data-type").with_attributes([
|
||||
("content-type", "text/vcard"),
|
||||
("version", "4.0"),
|
||||
])))?;
|
||||
xml_writer.write_event(Event::Empty(
|
||||
BytesStart::new("CR:address-data-type")
|
||||
.with_attributes([("content-type", "text/vcard"), ("version", "3.0")]),
|
||||
))?;
|
||||
xml_writer.write_event(Event::Empty(
|
||||
BytesStart::new("CR:address-data-type")
|
||||
.with_attributes([("content-type", "text/vcard"), ("version", "4.0")]),
|
||||
))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("CR:supported-address-data")))?;
|
||||
|
||||
// addressbook-description
|
||||
@@ -259,7 +282,9 @@ impl CardDavAdapter {
|
||||
}
|
||||
|
||||
// current-user-privilege-set
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:current-user-privilege-set")))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new(
|
||||
"D:current-user-privilege-set",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:read")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:privilege")))?;
|
||||
@@ -271,9 +296,7 @@ impl CardDavAdapter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_addressbook_prop_names<W: Write>(
|
||||
xml_writer: &mut Writer<W>,
|
||||
) -> Result<()> {
|
||||
fn write_addressbook_prop_names<W: Write>(xml_writer: &mut Writer<W>) -> Result<()> {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:displayname")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:getlastmodified")))?;
|
||||
@@ -281,7 +304,9 @@ impl CardDavAdapter {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("CR:supported-address-data")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("CR:addressbook-description")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:current-user-privilege-set")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(
|
||||
"D:current-user-privilege-set",
|
||||
)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -297,54 +322,66 @@ impl CardDavAdapter {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:collection")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("CR:addressbook")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:resourcetype")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "displayname") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:displayname")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&book.name)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:displayname")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getlastmodified") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&book.updated_at.to_rfc2822())))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&book.updated_at.to_rfc2822())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getetag") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", book.id))))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&format!("\"{}\"", book.id))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getcontenttype") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("text/vcard")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
},
|
||||
}
|
||||
("urn:ietf:params:xml:ns:carddav", "addressbook-description") => {
|
||||
if let Some(ref desc) = book.description {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("CR:addressbook-description")))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new(
|
||||
"CR:addressbook-description",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(desc)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("CR:addressbook-description")))?;
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("CR:addressbook-description")))?;
|
||||
} else {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("CR:addressbook-description")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(
|
||||
"CR:addressbook-description",
|
||||
)))?;
|
||||
}
|
||||
}
|
||||
},
|
||||
("urn:ietf:params:xml:ns:carddav", "supported-address-data") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("CR:supported-address-data")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("CR:address-data-type").with_attributes([
|
||||
("content-type", "text/vcard"),
|
||||
("version", "3.0"),
|
||||
])))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("CR:supported-address-data")))?;
|
||||
},
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("CR:supported-address-data")))?;
|
||||
xml_writer.write_event(Event::Empty(
|
||||
BytesStart::new("CR:address-data-type")
|
||||
.with_attributes([("content-type", "text/vcard"), ("version", "3.0")]),
|
||||
))?;
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("CR:supported-address-data")))?;
|
||||
}
|
||||
("DAV:", "current-user-privilege-set") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:current-user-privilege-set")))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new(
|
||||
"D:current-user-privilege-set",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:read")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:write")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:current-user-privilege-set")))?;
|
||||
},
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("D:current-user-privilege-set")))?;
|
||||
}
|
||||
_ => {
|
||||
let prop_name = if prop.namespace == "urn:ietf:params:xml:ns:carddav" {
|
||||
format!("CR:{}", prop.name)
|
||||
@@ -370,10 +407,12 @@ impl CardDavAdapter {
|
||||
) -> Result<()> {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:multistatus").with_attributes([
|
||||
xml_writer.write_event(Event::Start(
|
||||
BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
("xmlns:CR", "urn:ietf:params:xml:ns:carddav"),
|
||||
])))?;
|
||||
]),
|
||||
))?;
|
||||
|
||||
let props = match report {
|
||||
CardDavReportType::AddressbookQuery { props } => props.clone(),
|
||||
@@ -383,7 +422,8 @@ impl CardDavAdapter {
|
||||
|
||||
for contact in contacts {
|
||||
let href = format!("{}{}.vcf", base_href, contact.uid);
|
||||
let vcard = vcards.iter()
|
||||
let vcard = vcards
|
||||
.iter()
|
||||
.find(|(uid, _)| *uid == contact.uid)
|
||||
.map(|(_, data)| data.as_str())
|
||||
.unwrap_or("");
|
||||
@@ -420,7 +460,10 @@ impl CardDavAdapter {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", contact.etag))))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!(
|
||||
"\"{}\"",
|
||||
contact.etag
|
||||
))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
@@ -437,28 +480,37 @@ impl CardDavAdapter {
|
||||
match (prop.namespace.as_str(), prop.name.as_str()) {
|
||||
("DAV:", "resourcetype") => {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getetag") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", contact.etag))))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!(
|
||||
"\"{}\"",
|
||||
contact.etag
|
||||
))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getcontenttype") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("text/vcard; charset=utf-8")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(
|
||||
"text/vcard; charset=utf-8",
|
||||
)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
},
|
||||
}
|
||||
("DAV:", "getlastmodified") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&contact.updated_at.to_rfc2822())))?;
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(
|
||||
&contact.updated_at.to_rfc2822(),
|
||||
)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
},
|
||||
}
|
||||
("urn:ietf:params:xml:ns:carddav", "address-data") => {
|
||||
let vcard = contact_to_vcard(contact);
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("CR:address-data")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&vcard)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("CR:address-data")))?;
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
let prop_name = if prop.namespace == "urn:ietf:params:xml:ns:carddav" {
|
||||
format!("CR:{}", prop.name)
|
||||
@@ -486,7 +538,9 @@ impl CardDavAdapter {
|
||||
}
|
||||
|
||||
/// Parse a MKCOL XML request for making an address book
|
||||
pub fn parse_mkaddressbook<R: Read>(reader: R) -> Result<(String, Option<String>, Option<String>)> {
|
||||
pub fn parse_mkaddressbook<R: Read>(
|
||||
reader: R,
|
||||
) -> Result<(String, Option<String>, Option<String>)> {
|
||||
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
|
||||
xml_reader.config_mut().trim_text(true);
|
||||
|
||||
@@ -510,30 +564,43 @@ impl CardDavAdapter {
|
||||
match name_str {
|
||||
s if s == "set" || s.ends_with(":set") => in_set = true,
|
||||
s if in_set && (s == "prop" || s.ends_with(":prop")) => in_prop = true,
|
||||
s if in_prop && (s == "displayname" || s.ends_with(":displayname")) => in_displayname = true,
|
||||
s if in_prop && (s == "addressbook-description" || s.ends_with(":addressbook-description")) => in_description = true,
|
||||
s if in_prop && (s == "displayname" || s.ends_with(":displayname")) => {
|
||||
in_displayname = true
|
||||
}
|
||||
s if in_prop
|
||||
&& (s == "addressbook-description"
|
||||
|| s.ends_with(":addressbook-description")) =>
|
||||
{
|
||||
in_description = true
|
||||
}
|
||||
s if in_prop && (s.contains("color")) => in_color = true,
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::Text(e)) => {
|
||||
let text = e.decode().unwrap_or_default();
|
||||
if in_displayname { displayname = text.to_string(); }
|
||||
else if in_description { description = Some(text.to_string()); }
|
||||
else if in_color { color = Some(text.to_string()); }
|
||||
},
|
||||
if in_displayname {
|
||||
displayname = text.to_string();
|
||||
} else if in_description {
|
||||
description = Some(text.to_string());
|
||||
} else if in_color {
|
||||
color = Some(text.to_string());
|
||||
}
|
||||
}
|
||||
Ok(Event::End(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
match name_str {
|
||||
s if s == "set" || s.ends_with(":set") => in_set = false,
|
||||
s if s == "prop" || s.ends_with(":prop") => in_prop = false,
|
||||
s if s == "displayname" || s.ends_with(":displayname") => in_displayname = false,
|
||||
s if s == "displayname" || s.ends_with(":displayname") => {
|
||||
in_displayname = false
|
||||
}
|
||||
s if s.contains("description") => in_description = false,
|
||||
s if s.contains("color") => in_color = false,
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(WebDavError::XmlError(e)),
|
||||
_ => (),
|
||||
@@ -567,10 +634,13 @@ pub fn contact_to_vcard(contact: &ContactDto) -> String {
|
||||
vcard.push_str(&format!("FN:{}\r\n", fn_name));
|
||||
} else {
|
||||
// FN is mandatory in vCard 3.0
|
||||
let fn_name = format!("{} {}",
|
||||
let fn_name = format!(
|
||||
"{} {}",
|
||||
contact.first_name.as_deref().unwrap_or(""),
|
||||
contact.last_name.as_deref().unwrap_or(""),
|
||||
).trim().to_string();
|
||||
)
|
||||
.trim()
|
||||
.to_string();
|
||||
if !fn_name.is_empty() {
|
||||
vcard.push_str(&format!("FN:{}\r\n", fn_name));
|
||||
} else {
|
||||
@@ -583,22 +653,35 @@ pub fn contact_to_vcard(contact: &ContactDto) -> String {
|
||||
}
|
||||
|
||||
for email in &contact.email {
|
||||
vcard.push_str(&format!("EMAIL;TYPE={}:{}\r\n", email.r#type.to_uppercase(), email.email));
|
||||
vcard.push_str(&format!(
|
||||
"EMAIL;TYPE={}:{}\r\n",
|
||||
email.r#type.to_uppercase(),
|
||||
email.email
|
||||
));
|
||||
}
|
||||
|
||||
for phone in &contact.phone {
|
||||
vcard.push_str(&format!("TEL;TYPE={}:{}\r\n", phone.r#type.to_uppercase(), phone.number));
|
||||
vcard.push_str(&format!(
|
||||
"TEL;TYPE={}:{}\r\n",
|
||||
phone.r#type.to_uppercase(),
|
||||
phone.number
|
||||
));
|
||||
}
|
||||
|
||||
for addr in &contact.address {
|
||||
let adr = format!(";;{};{};{};{};{}",
|
||||
let adr = format!(
|
||||
";;{};{};{};{};{}",
|
||||
addr.street.as_deref().unwrap_or(""),
|
||||
addr.city.as_deref().unwrap_or(""),
|
||||
addr.state.as_deref().unwrap_or(""),
|
||||
addr.postal_code.as_deref().unwrap_or(""),
|
||||
addr.country.as_deref().unwrap_or(""),
|
||||
);
|
||||
vcard.push_str(&format!("ADR;TYPE={}:{}\r\n", addr.r#type.to_uppercase(), adr));
|
||||
vcard.push_str(&format!(
|
||||
"ADR;TYPE={}:{}\r\n",
|
||||
addr.r#type.to_uppercase(),
|
||||
adr
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(org) = &contact.organization {
|
||||
@@ -617,7 +700,10 @@ pub fn contact_to_vcard(contact: &ContactDto) -> String {
|
||||
vcard.push_str(&format!("PHOTO;VALUE=URI:{}\r\n", photo));
|
||||
}
|
||||
|
||||
vcard.push_str(&format!("REV:{}\r\n", contact.updated_at.format("%Y%m%dT%H%M%SZ")));
|
||||
vcard.push_str(&format!(
|
||||
"REV:{}\r\n",
|
||||
contact.updated_at.format("%Y%m%dT%H%M%SZ")
|
||||
));
|
||||
vcard.push_str("END:VCARD\r\n");
|
||||
|
||||
vcard
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
use chrono::{Utc, TimeZone, NaiveDate};
|
||||
use crate::application::adapters::carddav_adapter::{CardDavAdapter, CardDavReportType, contact_to_vcard};
|
||||
use crate::application::adapters::webdav_adapter::{PropFindRequest, PropFindType, QualifiedName};
|
||||
use crate::application::adapters::carddav_adapter::{
|
||||
CardDavAdapter, CardDavReportType, contact_to_vcard,
|
||||
};
|
||||
use crate::application::adapters::webdav_adapter::{
|
||||
PropFindRequest, PropFindType, QualifiedName,
|
||||
};
|
||||
use crate::application::dtos::address_book_dto::AddressBookDto;
|
||||
use crate::application::dtos::contact_dto::{ContactDto, EmailDto, PhoneDto, AddressDto};
|
||||
use crate::application::dtos::contact_dto::{AddressDto, ContactDto, EmailDto, PhoneDto};
|
||||
use chrono::{NaiveDate, TimeZone, Utc};
|
||||
use std::io::Cursor;
|
||||
|
||||
fn sample_address_book() -> AddressBookDto {
|
||||
AddressBookDto {
|
||||
@@ -41,15 +45,12 @@ mod tests {
|
||||
is_primary: false,
|
||||
},
|
||||
],
|
||||
phone: vec![
|
||||
PhoneDto {
|
||||
phone: vec![PhoneDto {
|
||||
number: "+1-555-0100".to_string(),
|
||||
r#type: "cell".to_string(),
|
||||
is_primary: true,
|
||||
},
|
||||
],
|
||||
address: vec![
|
||||
AddressDto {
|
||||
}],
|
||||
address: vec![AddressDto {
|
||||
street: Some("123 Main St".to_string()),
|
||||
city: Some("Springfield".to_string()),
|
||||
state: Some("IL".to_string()),
|
||||
@@ -57,8 +58,7 @@ mod tests {
|
||||
country: Some("US".to_string()),
|
||||
r#type: "home".to_string(),
|
||||
is_primary: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
organization: Some("Acme Corp".to_string()),
|
||||
title: Some("Software Engineer".to_string()),
|
||||
notes: Some("Met at conference".to_string()),
|
||||
@@ -104,20 +104,40 @@ mod tests {
|
||||
let contact = sample_contact();
|
||||
let vcard = contact_to_vcard(&contact);
|
||||
|
||||
assert!(vcard.starts_with("BEGIN:VCARD"), "vCard should start with BEGIN:VCARD");
|
||||
assert!(
|
||||
vcard.starts_with("BEGIN:VCARD"),
|
||||
"vCard should start with BEGIN:VCARD"
|
||||
);
|
||||
assert!(vcard.contains("VERSION:3.0"), "Should be vCard 3.0");
|
||||
assert!(vcard.contains("FN:John Doe"), "Should contain full name");
|
||||
assert!(vcard.contains("N:Doe;John"), "Should contain structured name");
|
||||
assert!(
|
||||
vcard.contains("N:Doe;John"),
|
||||
"Should contain structured name"
|
||||
);
|
||||
assert!(vcard.contains("NICKNAME:Johnny"), "Should contain nickname");
|
||||
assert!(vcard.contains("john@example.com"), "Should contain email");
|
||||
assert!(vcard.contains("+1-555-0100"), "Should contain phone number");
|
||||
assert!(vcard.contains("ORG:Acme Corp"), "Should contain organization");
|
||||
assert!(vcard.contains("TITLE:Software Engineer"), "Should contain title");
|
||||
assert!(vcard.contains("NOTE:Met at conference"), "Should contain notes");
|
||||
assert!(
|
||||
vcard.contains("ORG:Acme Corp"),
|
||||
"Should contain organization"
|
||||
);
|
||||
assert!(
|
||||
vcard.contains("TITLE:Software Engineer"),
|
||||
"Should contain title"
|
||||
);
|
||||
assert!(
|
||||
vcard.contains("NOTE:Met at conference"),
|
||||
"Should contain notes"
|
||||
);
|
||||
assert!(vcard.contains("BDAY:1990-05-15"), "Should contain birthday");
|
||||
assert!(vcard.contains("UID:uid-contact-001@oxicloud"), "Should contain UID");
|
||||
assert!(vcard.ends_with("END:VCARD\r\n") || vcard.trim_end().ends_with("END:VCARD"),
|
||||
"vCard should end with END:VCARD");
|
||||
assert!(
|
||||
vcard.contains("UID:uid-contact-001@oxicloud"),
|
||||
"Should contain UID"
|
||||
);
|
||||
assert!(
|
||||
vcard.ends_with("END:VCARD\r\n") || vcard.trim_end().ends_with("END:VCARD"),
|
||||
"vCard should end with END:VCARD"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -128,7 +148,10 @@ mod tests {
|
||||
assert!(vcard.contains("BEGIN:VCARD"), "Should start correctly");
|
||||
assert!(vcard.contains("VERSION:3.0"), "Should be vCard 3.0");
|
||||
assert!(vcard.contains("FN:Jane Smith"), "Should have full name");
|
||||
assert!(vcard.contains("UID:uid-contact-002@oxicloud"), "Should have UID");
|
||||
assert!(
|
||||
vcard.contains("UID:uid-contact-002@oxicloud"),
|
||||
"Should have UID"
|
||||
);
|
||||
assert!(vcard.contains("END:VCARD"), "Should end correctly");
|
||||
// Should NOT contain optional fields
|
||||
assert!(!vcard.contains("NICKNAME:"), "Should not have nickname");
|
||||
@@ -154,7 +177,11 @@ mod tests {
|
||||
</D:mkcol>"#;
|
||||
|
||||
let result = CardDavAdapter::parse_mkaddressbook(Cursor::new(xml));
|
||||
assert!(result.is_ok(), "Failed to parse mkaddressbook: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse mkaddressbook: {:?}",
|
||||
result.err()
|
||||
);
|
||||
let (name, desc, color) = result.unwrap();
|
||||
assert_eq!(name, "Work Contacts");
|
||||
assert_eq!(desc, Some("Colleagues and clients".to_string()));
|
||||
@@ -195,7 +222,11 @@ mod tests {
|
||||
</CR:addressbook-query>"#;
|
||||
|
||||
let result = CardDavAdapter::parse_report(Cursor::new(xml));
|
||||
assert!(result.is_ok(), "Failed to parse addressbook-query: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse addressbook-query: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
match result.unwrap() {
|
||||
CardDavReportType::AddressbookQuery { props } => {
|
||||
@@ -219,7 +250,11 @@ mod tests {
|
||||
</CR:addressbook-multiget>"#;
|
||||
|
||||
let result = CardDavAdapter::parse_report(Cursor::new(xml));
|
||||
assert!(result.is_ok(), "Failed to parse multiget: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse multiget: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
match result.unwrap() {
|
||||
CardDavReportType::AddressbookMultiget { hrefs, props } => {
|
||||
@@ -251,12 +286,25 @@ mod tests {
|
||||
"/carddav",
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "Failed to generate propfind response: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to generate propfind response: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
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");
|
||||
assert!(xml_str.contains("ab-001"), "Should contain address book ID in href");
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -277,11 +325,21 @@ mod tests {
|
||||
"0",
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "Failed to generate depth-0 propfind: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to generate depth-0 propfind: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
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");
|
||||
assert!(
|
||||
xml_str.contains("multistatus"),
|
||||
"Should contain multistatus"
|
||||
);
|
||||
assert!(
|
||||
xml_str.contains("My Contacts"),
|
||||
"Should contain address book name"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -302,14 +360,30 @@ mod tests {
|
||||
"1",
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "Failed to generate depth-1 propfind: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to generate depth-1 propfind: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
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");
|
||||
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
|
||||
assert!(xml_str.contains("contact-001"), "Should include contact-001");
|
||||
assert!(xml_str.contains("contact-002"), "Should include contact-002");
|
||||
assert!(
|
||||
xml_str.contains("contact-001"),
|
||||
"Should include contact-001"
|
||||
);
|
||||
assert!(
|
||||
xml_str.contains("contact-002"),
|
||||
"Should include contact-002"
|
||||
);
|
||||
}
|
||||
|
||||
// ========================
|
||||
@@ -319,9 +393,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_generate_contacts_response() {
|
||||
let contacts = vec![sample_contact()];
|
||||
let vcards = vec![
|
||||
("contact-001".to_string(), contact_to_vcard(&sample_contact())),
|
||||
];
|
||||
let vcards = vec![(
|
||||
"contact-001".to_string(),
|
||||
contact_to_vcard(&sample_contact()),
|
||||
)];
|
||||
let report = CardDavReportType::AddressbookQuery {
|
||||
props: vec![
|
||||
QualifiedName {
|
||||
@@ -344,10 +419,17 @@ mod tests {
|
||||
"/carddav/ab-001",
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "Failed to generate contacts response: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to generate contacts response: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let xml_str = String::from_utf8(output).expect("Invalid UTF-8");
|
||||
assert!(xml_str.contains("multistatus"), "Should contain multistatus");
|
||||
assert!(
|
||||
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");
|
||||
}
|
||||
@@ -356,9 +438,7 @@ mod tests {
|
||||
fn test_generate_empty_contacts_response() {
|
||||
let contacts: Vec<ContactDto> = vec![];
|
||||
let vcards: Vec<(String, String)> = vec![];
|
||||
let report = CardDavReportType::AddressbookQuery {
|
||||
props: vec![],
|
||||
};
|
||||
let report = CardDavReportType::AddressbookQuery { props: vec![] };
|
||||
|
||||
let mut output = Vec::new();
|
||||
let result = CardDavAdapter::generate_contacts_response(
|
||||
@@ -369,7 +449,10 @@ mod tests {
|
||||
"/carddav/ab-001",
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "Empty contacts should produce valid response");
|
||||
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");
|
||||
}
|
||||
@@ -399,8 +482,14 @@ mod tests {
|
||||
|
||||
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("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");
|
||||
}
|
||||
@@ -416,7 +505,10 @@ mod tests {
|
||||
|
||||
// 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");
|
||||
assert!(
|
||||
vcard.contains("john.doe@personal.com"),
|
||||
"Should have personal email"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Adapters module for translating between external protocols and internal models
|
||||
|
||||
pub mod webdav_adapter;
|
||||
pub mod caldav_adapter;
|
||||
pub mod carddav_adapter;
|
||||
pub mod webdav_adapter;
|
||||
|
||||
#[cfg(test)]
|
||||
mod caldav_adapter_test;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::dtos::folder_dto::FolderDto;
|
||||
use chrono::Utc;
|
||||
use quick_xml::{
|
||||
Reader, Writer,
|
||||
events::{BytesEnd, BytesStart, BytesText, Event},
|
||||
};
|
||||
/**
|
||||
* WebDAV Adapter Module
|
||||
*
|
||||
* This module provides conversion between WebDAV protocol XML structures and OxiCloud domain objects.
|
||||
* It handles parsing WebDAV request XML and generating WebDAV response XML according to RFC 4918.
|
||||
*/
|
||||
|
||||
use std::io::{Read, Write, BufReader};
|
||||
use quick_xml::{Reader, Writer, events::{Event, BytesStart, BytesEnd, BytesText}};
|
||||
use chrono::Utc;
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::dtos::folder_dto::FolderDto;
|
||||
use std::io::{BufReader, Read, Write};
|
||||
|
||||
/// Result type for WebDAV operations
|
||||
pub type Result<T> = std::result::Result<T, WebDavError>;
|
||||
@@ -142,9 +144,13 @@ impl WebDavAdapter {
|
||||
in_propfind = true;
|
||||
} else if in_propfind && (name_str == "prop" || name_str.ends_with(":prop")) {
|
||||
in_prop = true;
|
||||
} else if in_propfind && (name_str == "allprop" || name_str.ends_with(":allprop")) {
|
||||
} else if in_propfind
|
||||
&& (name_str == "allprop" || name_str.ends_with(":allprop"))
|
||||
{
|
||||
in_allprop = true;
|
||||
} else if in_propfind && (name_str == "propname" || name_str.ends_with(":propname")) {
|
||||
} else if in_propfind
|
||||
&& (name_str == "propname" || name_str.ends_with(":propname"))
|
||||
{
|
||||
in_propname = true;
|
||||
} else if in_prop {
|
||||
// Add property to request
|
||||
@@ -153,7 +159,7 @@ impl WebDavAdapter {
|
||||
|
||||
props.push(QualifiedName::new(namespace, prop_name));
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::End(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
@@ -167,14 +173,16 @@ impl WebDavAdapter {
|
||||
} else if name_str == "propname" || name_str.ends_with(":propname") {
|
||||
in_propname = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::Empty(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
if in_propfind && (name_str == "allprop" || name_str.ends_with(":allprop")) {
|
||||
in_allprop = true;
|
||||
} else if in_propfind && (name_str == "propname" || name_str.ends_with(":propname")) {
|
||||
} else if in_propfind
|
||||
&& (name_str == "propname" || name_str.ends_with(":propname"))
|
||||
{
|
||||
in_propname = true;
|
||||
} else if in_prop {
|
||||
// Add property to request (empty element)
|
||||
@@ -183,7 +191,7 @@ impl WebDavAdapter {
|
||||
|
||||
props.push(QualifiedName::new(namespace, prop_name));
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(WebDavError::XmlError(e)),
|
||||
_ => (),
|
||||
@@ -216,9 +224,9 @@ impl WebDavAdapter {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
// Start multistatus response
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
])))?;
|
||||
xml_writer.write_event(Event::Start(
|
||||
BytesStart::new("D:multistatus").with_attributes([("xmlns:D", "DAV:")]),
|
||||
))?;
|
||||
|
||||
// Add response for current folder if provided
|
||||
if let Some(folder) = folder {
|
||||
@@ -229,12 +237,22 @@ impl WebDavAdapter {
|
||||
if _depth != "0" {
|
||||
// Add responses for files
|
||||
for file in files {
|
||||
Self::write_file_response(&mut xml_writer, file, request, &format!("{}{}", base_href, file.name))?;
|
||||
Self::write_file_response(
|
||||
&mut xml_writer,
|
||||
file,
|
||||
request,
|
||||
&format!("{}{}", base_href, file.name),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Add responses for subfolders
|
||||
for subfolder in subfolders {
|
||||
Self::write_folder_response(&mut xml_writer, subfolder, request, &format!("{}{}/", base_href, subfolder.name))?;
|
||||
Self::write_folder_response(
|
||||
&mut xml_writer,
|
||||
subfolder,
|
||||
request,
|
||||
&format!("{}{}/", base_href, subfolder.name),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,9 +273,9 @@ impl WebDavAdapter {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
// Start multistatus response
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
])))?;
|
||||
xml_writer.write_event(Event::Start(
|
||||
BytesStart::new("D:multistatus").with_attributes([("xmlns:D", "DAV:")]),
|
||||
))?;
|
||||
|
||||
// Add response for file
|
||||
Self::write_file_response(&mut xml_writer, file, request, href)?;
|
||||
@@ -294,11 +312,11 @@ impl WebDavAdapter {
|
||||
PropFindType::AllProp => {
|
||||
// Write all standard properties for a folder
|
||||
Self::write_folder_standard_props(xml_writer, folder)?;
|
||||
},
|
||||
}
|
||||
PropFindType::PropName => {
|
||||
// Write only property names (empty elements)
|
||||
Self::write_folder_prop_names(xml_writer)?;
|
||||
},
|
||||
}
|
||||
PropFindType::Prop(props) => {
|
||||
// Write requested properties
|
||||
Self::write_folder_requested_props(xml_writer, folder, props)?;
|
||||
@@ -348,11 +366,11 @@ impl WebDavAdapter {
|
||||
PropFindType::AllProp => {
|
||||
// Write all standard properties for a file
|
||||
Self::write_file_standard_props(xml_writer, file)?;
|
||||
},
|
||||
}
|
||||
PropFindType::PropName => {
|
||||
// Write only property names (empty elements)
|
||||
Self::write_file_prop_names(xml_writer)?;
|
||||
},
|
||||
}
|
||||
PropFindType::Prop(props) => {
|
||||
// Write requested properties
|
||||
Self::write_file_requested_props(xml_writer, file, props)?;
|
||||
@@ -481,9 +499,7 @@ impl WebDavAdapter {
|
||||
}
|
||||
|
||||
/// Write folder property names
|
||||
fn write_folder_prop_names<W: Write>(
|
||||
xml_writer: &mut Writer<W>,
|
||||
) -> Result<()> {
|
||||
fn write_folder_prop_names<W: Write>(xml_writer: &mut Writer<W>) -> Result<()> {
|
||||
// Write empty property elements for folders
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:displayname")))?;
|
||||
@@ -497,9 +513,7 @@ impl WebDavAdapter {
|
||||
}
|
||||
|
||||
/// Write file property names
|
||||
fn write_file_prop_names<W: Write>(
|
||||
xml_writer: &mut Writer<W>,
|
||||
) -> Result<()> {
|
||||
fn write_file_prop_names<W: Write>(xml_writer: &mut Writer<W>) -> Result<()> {
|
||||
// Write empty property elements for files
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:displayname")))?;
|
||||
@@ -525,55 +539,72 @@ impl WebDavAdapter {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:resourcetype")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:collection")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:resourcetype")))?;
|
||||
},
|
||||
}
|
||||
"displayname" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:displayname")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&folder.name)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:displayname")))?;
|
||||
},
|
||||
}
|
||||
"creationdate" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:creationdate")))?;
|
||||
|
||||
// Convert u64 timestamp to DateTime
|
||||
let created_at = chrono::DateTime::<Utc>::from_timestamp(folder.created_at as i64, 0)
|
||||
let created_at =
|
||||
chrono::DateTime::<Utc>::from_timestamp(folder.created_at as i64, 0)
|
||||
.unwrap_or_else(Utc::now);
|
||||
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&created_at.to_rfc3339())))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&created_at.to_rfc3339())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:creationdate")))?;
|
||||
},
|
||||
}
|
||||
"getlastmodified" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
|
||||
// Convert u64 timestamp to DateTime
|
||||
let modified_at = chrono::DateTime::<Utc>::from_timestamp(folder.modified_at as i64, 0)
|
||||
let modified_at =
|
||||
chrono::DateTime::<Utc>::from_timestamp(folder.modified_at as i64, 0)
|
||||
.unwrap_or_else(Utc::now);
|
||||
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&modified_at.to_rfc2822())))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&modified_at.to_rfc2822())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
},
|
||||
}
|
||||
"getetag" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", folder.id))))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!(
|
||||
"\"{}\"",
|
||||
folder.id
|
||||
))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
},
|
||||
}
|
||||
"getcontentlength" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontentlength")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:getcontentlength")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("0")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontentlength")))?;
|
||||
},
|
||||
}
|
||||
"getcontenttype" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("httpd/unix-directory")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new("httpd/unix-directory")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
// Property not supported - write empty element
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(format!("D:{}", prop.name))))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(format!(
|
||||
"D:{}",
|
||||
prop.name
|
||||
))))?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-DAV namespace, not supported
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(format!("{}:{}", prop.namespace, prop.name))))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(format!(
|
||||
"{}:{}",
|
||||
prop.namespace, prop.name
|
||||
))))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,55 +622,72 @@ impl WebDavAdapter {
|
||||
match prop.name.as_str() {
|
||||
"resourcetype" => {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
},
|
||||
}
|
||||
"displayname" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:displayname")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&file.name)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:displayname")))?;
|
||||
},
|
||||
}
|
||||
"getcontenttype" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&file.mime_type)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
},
|
||||
}
|
||||
"getcontentlength" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontentlength")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&file.size.to_string())))?;
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:getcontentlength")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&file.size.to_string())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontentlength")))?;
|
||||
},
|
||||
}
|
||||
"creationdate" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:creationdate")))?;
|
||||
|
||||
// Convert u64 timestamp to DateTime
|
||||
let created_at = chrono::DateTime::<Utc>::from_timestamp(file.created_at as i64, 0)
|
||||
let created_at =
|
||||
chrono::DateTime::<Utc>::from_timestamp(file.created_at as i64, 0)
|
||||
.unwrap_or_else(Utc::now);
|
||||
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&created_at.to_rfc3339())))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&created_at.to_rfc3339())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:creationdate")))?;
|
||||
},
|
||||
}
|
||||
"getlastmodified" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
|
||||
// Convert u64 timestamp to DateTime
|
||||
let modified_at = chrono::DateTime::<Utc>::from_timestamp(file.modified_at as i64, 0)
|
||||
let modified_at =
|
||||
chrono::DateTime::<Utc>::from_timestamp(file.modified_at as i64, 0)
|
||||
.unwrap_or_else(Utc::now);
|
||||
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&modified_at.to_rfc2822())))?;
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&modified_at.to_rfc2822())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
},
|
||||
}
|
||||
"getetag" => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", file.id))))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!(
|
||||
"\"{}\"",
|
||||
file.id
|
||||
))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
// Property not supported - write empty element
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(format!("D:{}", prop.name))))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(format!(
|
||||
"D:{}",
|
||||
prop.name
|
||||
))))?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-DAV namespace, not supported
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(format!("{}:{}", prop.namespace, prop.name))))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(format!(
|
||||
"{}:{}",
|
||||
prop.namespace, prop.name
|
||||
))))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,10 +716,18 @@ impl WebDavAdapter {
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
match name_str {
|
||||
s if s == "propertyupdate" || s.ends_with(":propertyupdate") => in_propertyupdate = true,
|
||||
s if (in_propertyupdate && (s == "set" || s.ends_with(":set"))) => in_set = true,
|
||||
s if (in_propertyupdate && (s == "remove" || s.ends_with(":remove"))) => in_remove = true,
|
||||
s if ((in_set || in_remove) && (s == "prop" || s.ends_with(":prop"))) => in_prop = true,
|
||||
s if s == "propertyupdate" || s.ends_with(":propertyupdate") => {
|
||||
in_propertyupdate = true
|
||||
}
|
||||
s if (in_propertyupdate && (s == "set" || s.ends_with(":set"))) => {
|
||||
in_set = true
|
||||
}
|
||||
s if (in_propertyupdate && (s == "remove" || s.ends_with(":remove"))) => {
|
||||
in_remove = true
|
||||
}
|
||||
s if ((in_set || in_remove) && (s == "prop" || s.ends_with(":prop"))) => {
|
||||
in_prop = true
|
||||
}
|
||||
_ if in_prop => {
|
||||
// This is a property element
|
||||
let namespace = Self::extract_namespace(name_str);
|
||||
@@ -680,20 +736,22 @@ impl WebDavAdapter {
|
||||
current_prop = Some(QualifiedName::new(namespace, prop_name));
|
||||
current_text.clear();
|
||||
}
|
||||
_ => ()
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Event::Text(e)) => {
|
||||
if current_prop.is_some() {
|
||||
current_text.push_str(&e.decode().unwrap_or_default());
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::End(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
match name_str {
|
||||
s if s == "propertyupdate" || s.ends_with(":propertyupdate") => in_propertyupdate = false,
|
||||
s if s == "propertyupdate" || s.ends_with(":propertyupdate") => {
|
||||
in_propertyupdate = false
|
||||
}
|
||||
s if s == "set" || s.ends_with(":set") => in_set = false,
|
||||
s if s == "remove" || s.ends_with(":remove") => in_remove = false,
|
||||
s if s == "prop" || s.ends_with(":prop") => in_prop = false,
|
||||
@@ -703,7 +761,11 @@ impl WebDavAdapter {
|
||||
if in_set {
|
||||
props_to_set.push(PropValue {
|
||||
name: prop_name,
|
||||
value: if current_text.is_empty() { None } else { Some(current_text.clone()) },
|
||||
value: if current_text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(current_text.clone())
|
||||
},
|
||||
});
|
||||
} else if in_remove {
|
||||
props_to_remove.push(prop_name);
|
||||
@@ -711,9 +773,9 @@ impl WebDavAdapter {
|
||||
}
|
||||
current_text.clear();
|
||||
}
|
||||
_ => ()
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Event::Empty(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
@@ -734,7 +796,7 @@ impl WebDavAdapter {
|
||||
props_to_remove.push(qname);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(WebDavError::XmlError(e)),
|
||||
_ => (),
|
||||
@@ -755,9 +817,9 @@ impl WebDavAdapter {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
// Start multistatus response
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
])))?;
|
||||
xml_writer.write_event(Event::Start(
|
||||
BytesStart::new("D:multistatus").with_attributes([("xmlns:D", "DAV:")]),
|
||||
))?;
|
||||
|
||||
// Start response element
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:response")))?;
|
||||
@@ -868,20 +930,32 @@ impl WebDavAdapter {
|
||||
|
||||
match name_str {
|
||||
s if s == "lockinfo" || s.ends_with(":lockinfo") => in_lockinfo = true,
|
||||
s if in_lockinfo && (s == "lockscope" || s.ends_with(":lockscope")) => in_lockscope = true,
|
||||
s if in_lockinfo && (s == "locktype" || s.ends_with(":locktype")) => in_locktype = true,
|
||||
s if in_lockinfo && (s == "owner" || s.ends_with(":owner")) => in_owner = true,
|
||||
s if in_lockscope && (s == "exclusive" || s.ends_with(":exclusive")) => scope = LockScope::Exclusive,
|
||||
s if in_lockscope && (s == "shared" || s.ends_with(":shared")) => scope = LockScope::Shared,
|
||||
s if in_locktype && (s == "write" || s.ends_with(":write")) => type_ = LockType::Write,
|
||||
_ => ()
|
||||
s if in_lockinfo && (s == "lockscope" || s.ends_with(":lockscope")) => {
|
||||
in_lockscope = true
|
||||
}
|
||||
s if in_lockinfo && (s == "locktype" || s.ends_with(":locktype")) => {
|
||||
in_locktype = true
|
||||
}
|
||||
s if in_lockinfo && (s == "owner" || s.ends_with(":owner")) => {
|
||||
in_owner = true
|
||||
}
|
||||
s if in_lockscope && (s == "exclusive" || s.ends_with(":exclusive")) => {
|
||||
scope = LockScope::Exclusive
|
||||
}
|
||||
s if in_lockscope && (s == "shared" || s.ends_with(":shared")) => {
|
||||
scope = LockScope::Shared
|
||||
}
|
||||
s if in_locktype && (s == "write" || s.ends_with(":write")) => {
|
||||
type_ = LockType::Write
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Event::Text(e)) => {
|
||||
if in_owner {
|
||||
owner_text.push_str(&e.decode().unwrap_or_default());
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(Event::End(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
@@ -891,20 +965,26 @@ impl WebDavAdapter {
|
||||
s if s == "lockscope" || s.ends_with(":lockscope") => in_lockscope = false,
|
||||
s if s == "locktype" || s.ends_with(":locktype") => in_locktype = false,
|
||||
s if s == "owner" || s.ends_with(":owner") => in_owner = false,
|
||||
_ => ()
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Event::Empty(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
match name_str {
|
||||
s if in_lockscope && (s == "exclusive" || s.ends_with(":exclusive")) => scope = LockScope::Exclusive,
|
||||
s if in_lockscope && (s == "shared" || s.ends_with(":shared")) => scope = LockScope::Shared,
|
||||
s if in_locktype && (s == "write" || s.ends_with(":write")) => type_ = LockType::Write,
|
||||
_ => ()
|
||||
s if in_lockscope && (s == "exclusive" || s.ends_with(":exclusive")) => {
|
||||
scope = LockScope::Exclusive
|
||||
}
|
||||
s if in_lockscope && (s == "shared" || s.ends_with(":shared")) => {
|
||||
scope = LockScope::Shared
|
||||
}
|
||||
s if in_locktype && (s == "write" || s.ends_with(":write")) => {
|
||||
type_ = LockType::Write
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(WebDavError::XmlError(e)),
|
||||
_ => (),
|
||||
@@ -913,7 +993,11 @@ impl WebDavAdapter {
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
let owner = if owner_text.is_empty() { None } else { Some(owner_text) };
|
||||
let owner = if owner_text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(owner_text)
|
||||
};
|
||||
|
||||
Ok((scope, type_, owner))
|
||||
}
|
||||
@@ -927,9 +1011,9 @@ impl WebDavAdapter {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
// Start prop element (direct response, not multistatus)
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:prop").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
])))?;
|
||||
xml_writer.write_event(Event::Start(
|
||||
BytesStart::new("D:prop").with_attributes([("xmlns:D", "DAV:")]),
|
||||
))?;
|
||||
|
||||
// Start lockdiscovery
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:lockdiscovery")))?;
|
||||
@@ -947,7 +1031,7 @@ impl WebDavAdapter {
|
||||
match lock_info.scope {
|
||||
LockScope::Exclusive => {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:exclusive")))?;
|
||||
},
|
||||
}
|
||||
LockScope::Shared => {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:shared")))?;
|
||||
}
|
||||
@@ -998,7 +1082,8 @@ impl WebDavAdapter {
|
||||
/// Helper method to extract namespace from tag name
|
||||
pub fn extract_namespace(name: &str) -> String {
|
||||
if let Some(idx) = name.rfind(':')
|
||||
&& idx > 0 {
|
||||
&& idx > 0
|
||||
{
|
||||
return name[..idx].to_string();
|
||||
}
|
||||
// Default namespace for WebDAV
|
||||
@@ -1008,8 +1093,10 @@ impl WebDavAdapter {
|
||||
/// Helper method to extract local name from tag name
|
||||
pub fn extract_local_name(name: &str) -> String {
|
||||
if let Some(idx) = name.rfind(':')
|
||||
&& idx > 0 && idx < name.len() - 1 {
|
||||
return name[idx+1..].to_string();
|
||||
&& idx > 0
|
||||
&& idx < name.len() - 1
|
||||
{
|
||||
return name[idx + 1..].to_string();
|
||||
}
|
||||
name.to_string()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::domain::entities::contact::AddressBook;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::domain::entities::contact::AddressBook;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddressBookDto {
|
||||
|
||||
@@ -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_event::CalendarEvent;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// DTO for calendar data transfer
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::domain::entities::contact::{Address, Contact, ContactGroup, Email, Phone};
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::domain::entities::contact::{Contact, Email, Phone, Address, ContactGroup};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EmailDto {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// DTO for favorites item
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::domain::entities::file::File;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// DTO for file responses
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -58,7 +58,7 @@ impl From<FileDto> for File {
|
||||
dto.mime_type,
|
||||
dto.folder_id,
|
||||
dto.created_at,
|
||||
dto.modified_at
|
||||
dto.modified_at,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::domain::entities::folder::Folder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// DTO for folder creation requests
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -77,7 +77,7 @@ impl From<FolderDto> for Folder {
|
||||
dto.path,
|
||||
dto.parent_id,
|
||||
dto.created_at,
|
||||
dto.modified_at
|
||||
dto.modified_at,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::domain::services::i18n_service::Locale;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// DTO for locale information
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -12,4 +12,3 @@ pub mod settings_dto;
|
||||
pub mod share_dto;
|
||||
pub mod trash_dto;
|
||||
pub mod user_dto;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A DTO to represent pagination information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -79,21 +79,13 @@ impl PaginationRequestDto {
|
||||
page_size = 500;
|
||||
}
|
||||
|
||||
Self {
|
||||
page,
|
||||
page_size,
|
||||
}
|
||||
Self { page, page_size }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PaginatedResponseDto<T> {
|
||||
/// Creates a new paginated response from the data and pagination information
|
||||
pub fn new(
|
||||
items: Vec<T>,
|
||||
page: usize,
|
||||
page_size: usize,
|
||||
total_items: usize,
|
||||
) -> Self {
|
||||
pub fn new(items: Vec<T>, page: usize, page_size: usize, total_items: usize) -> Self {
|
||||
let total_pages = if total_items == 0 {
|
||||
0
|
||||
} else {
|
||||
@@ -109,9 +101,6 @@ impl<T> PaginatedResponseDto<T> {
|
||||
has_prev: page > 0,
|
||||
};
|
||||
|
||||
Self {
|
||||
items,
|
||||
pagination,
|
||||
}
|
||||
Self { items, pagination }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// DTO for recent items
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/**
|
||||
* Data Transfer Object for file search criteria.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ============================================================================
|
||||
// OIDC Settings DTOs (Admin Panel)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::domain::entities::user::User;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserDto {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod adapters;
|
||||
pub mod dtos;
|
||||
pub mod ports;
|
||||
pub mod services;
|
||||
pub mod transactions;
|
||||
pub mod adapters;
|
||||
|
||||
// Re-exportaciones para facilitar el acceso a los principales puertos
|
||||
@@ -1,7 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::domain::entities::user::User;
|
||||
use crate::domain::entities::session::Session;
|
||||
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
|
||||
@@ -81,7 +81,11 @@ pub trait UserStoragePort: Send + Sync + 'static {
|
||||
async fn update_user(&self, user: User) -> Result<User, DomainError>;
|
||||
|
||||
/// 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
|
||||
async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<User>, DomainError>;
|
||||
@@ -96,7 +100,11 @@ pub trait UserStoragePort: Send + Sync + 'static {
|
||||
async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// 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
|
||||
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>;
|
||||
|
||||
/// 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
|
||||
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.
|
||||
/// Includes PKCE code_challenge (S256) and nonce for ID token binding.
|
||||
/// 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.
|
||||
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.
|
||||
/// 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)
|
||||
async fn fetch_user_info(&self, access_token: &str) -> Result<OidcIdClaims, DomainError>;
|
||||
@@ -161,7 +186,10 @@ pub trait SessionStoragePort: Send + Sync + 'static {
|
||||
async fn create_session(&self, session: Session) -> Result<Session, DomainError>;
|
||||
|
||||
/// 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
|
||||
async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
//! The application and interface layers remain independent of the caching
|
||||
//! implementation details.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use crate::common::errors::DomainError;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use crate::common::errors::DomainError;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Statistics for monitoring write-behind cache status.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
||||
@@ -1,47 +1,108 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::application::dtos::calendar_dto::{
|
||||
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto,
|
||||
CreateEventDto, UpdateEventDto, CreateEventICalDto
|
||||
CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto,
|
||||
UpdateCalendarDto, UpdateEventDto,
|
||||
};
|
||||
use crate::common::errors::DomainError;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// Port for external calendar storage mechanisms
|
||||
#[async_trait]
|
||||
pub trait CalendarStoragePort: Send + Sync + 'static {
|
||||
// Calendar operations
|
||||
async fn create_calendar(&self, calendar: CreateCalendarDto, owner_id: &str) -> Result<CalendarDto, DomainError>;
|
||||
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError>;
|
||||
async fn create_calendar(
|
||||
&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 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_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>;
|
||||
async fn list_calendars_by_owner(
|
||||
&self,
|
||||
owner_id: &str,
|
||||
) -> 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
|
||||
async fn share_calendar(&self, 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>;
|
||||
async fn share_calendar(
|
||||
&self,
|
||||
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
|
||||
async fn set_calendar_property(&self, 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>;
|
||||
async fn set_calendar_property(
|
||||
&self,
|
||||
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
|
||||
async fn create_event(&self, event: CreateEventDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn create_event_from_ical(&self, event: CreateEventICalDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn create_event_from_ical(
|
||||
&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 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_paginated(&self, calendar_id: &str, limit: i64, offset: i64) -> Result<Vec<CalendarEventDto>, DomainError>;
|
||||
async fn list_events_by_calendar(
|
||||
&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(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
start: &DateTime<Utc>,
|
||||
end: &DateTime<Utc>
|
||||
end: &DateTime<Utc>,
|
||||
) -> Result<Vec<CalendarEventDto>, DomainError>;
|
||||
}
|
||||
|
||||
@@ -49,41 +110,113 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
|
||||
#[async_trait]
|
||||
pub trait CalendarUseCase: Send + Sync + 'static {
|
||||
// Calendar operations
|
||||
async fn create_calendar(&self, calendar: CreateCalendarDto) -> Result<CalendarDto, DomainError>;
|
||||
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError>;
|
||||
async fn create_calendar(
|
||||
&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 get_calendar(&self, calendar_id: &str) -> Result<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_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
|
||||
async fn share_calendar(&self, 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>;
|
||||
async fn share_calendar(
|
||||
&self,
|
||||
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
|
||||
async fn create_event(&self, event: CreateEventDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn create_event_from_ical(&self, event: CreateEventICalDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn create_event_from_ical(
|
||||
&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 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(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
limit: Option<i64>,
|
||||
offset: Option<i64>,
|
||||
) -> Result<Vec<CalendarEventDto>, DomainError>;
|
||||
async fn get_events_in_range(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
start: DateTime<Utc>,
|
||||
end: DateTime<Utc>
|
||||
end: DateTime<Utc>,
|
||||
) -> Result<Vec<CalendarEventDto>, DomainError>;
|
||||
|
||||
// ─── User-contextualized variants (for CalDAV protocol handler) ──
|
||||
async fn create_calendar_for_user(&self, calendar: CreateCalendarDto, user_id: &str) -> Result<CalendarDto, DomainError>;
|
||||
async fn update_calendar_for_user(&self, calendar_id: &str, 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>;
|
||||
async fn create_calendar_for_user(
|
||||
&self,
|
||||
calendar: CreateCalendarDto,
|
||||
user_id: &str,
|
||||
) -> Result<CalendarDto, DomainError>;
|
||||
async fn update_calendar_for_user(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
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>;
|
||||
}
|
||||
@@ -1,57 +1,143 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::application::dtos::address_book_dto::{
|
||||
AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto,
|
||||
ShareAddressBookDto, UnshareAddressBookDto
|
||||
AddressBookDto, CreateAddressBookDto, ShareAddressBookDto, UnshareAddressBookDto,
|
||||
UpdateAddressBookDto,
|
||||
};
|
||||
use crate::application::dtos::contact_dto::{
|
||||
ContactDto, CreateContactDto, UpdateContactDto, CreateContactVCardDto,
|
||||
ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto
|
||||
ContactDto, ContactGroupDto, CreateContactDto, CreateContactGroupDto, CreateContactVCardDto,
|
||||
GroupMembershipDto, UpdateContactDto, UpdateContactGroupDto,
|
||||
};
|
||||
use crate::common::errors::DomainError;
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub type CardDavRepositoryError = DomainError;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AddressBookUseCase: Send + Sync + 'static {
|
||||
// Address Book operations
|
||||
async fn create_address_book(&self, dto: CreateAddressBookDto) -> Result<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 create_address_book(
|
||||
&self,
|
||||
dto: CreateAddressBookDto,
|
||||
) -> Result<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>;
|
||||
|
||||
// Address Book sharing
|
||||
async fn share_address_book(&self, 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 fn share_address_book(
|
||||
&self,
|
||||
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]
|
||||
pub trait ContactUseCase: Send + Sync + 'static {
|
||||
// Contact operations
|
||||
async fn create_contact(&self, dto: CreateContactDto) -> Result<ContactDto, DomainError>;
|
||||
async fn create_contact_from_vcard(&self, dto: CreateContactVCardDto) -> Result<ContactDto, DomainError>;
|
||||
async fn update_contact(&self, contact_id: &str, update: UpdateContactDto) -> Result<ContactDto, DomainError>;
|
||||
async fn create_contact_from_vcard(
|
||||
&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 get_contact(&self, contact_id: &str, user_id: &str) -> Result<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>;
|
||||
async fn get_contact(&self, contact_id: &str, user_id: &str)
|
||||
-> Result<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
|
||||
async fn create_group(&self, dto: CreateContactGroupDto) -> Result<ContactGroupDto, DomainError>;
|
||||
async fn update_group(&self, group_id: &str, update: UpdateContactGroupDto) -> Result<ContactGroupDto, DomainError>;
|
||||
async fn create_group(
|
||||
&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 get_group(&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>;
|
||||
async fn get_group(
|
||||
&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
|
||||
async fn add_contact_to_group(&self, dto: GroupMembershipDto, 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>;
|
||||
async fn add_contact_to_group(
|
||||
&self,
|
||||
dto: GroupMembershipDto,
|
||||
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
|
||||
async fn get_contact_vcard(&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>;
|
||||
async fn get_contact_vcard(
|
||||
&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>;
|
||||
}
|
||||
@@ -4,11 +4,11 @@
|
||||
//! operations, keeping the application and interface layers independent of
|
||||
//! 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 bytes::Bytes;
|
||||
use serde::Serialize;
|
||||
use crate::common::errors::DomainError;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Default chunk size (5 MB) — optimised for parallel transfers.
|
||||
pub const DEFAULT_CHUNK_SIZE: usize = 5 * 1024 * 1024;
|
||||
@@ -80,10 +80,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
|
||||
) -> Result<ChunkUploadResponseDto, DomainError>;
|
||||
|
||||
/// Get the current status of an upload session.
|
||||
async fn get_status(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<UploadStatusResponseDto, DomainError>;
|
||||
async fn get_status(&self, upload_id: &str) -> Result<UploadStatusResponseDto, DomainError>;
|
||||
|
||||
/// 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>;
|
||||
|
||||
/// Finalize upload: clean up the session and temporary files.
|
||||
async fn finalize_upload(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<(), DomainError>;
|
||||
async fn finalize_upload(&self, upload_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Cancel an upload and clean up all temporary data.
|
||||
async fn cancel_upload(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<(), DomainError>;
|
||||
async fn cancel_upload(&self, upload_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Check if a file size qualifies for chunked upload.
|
||||
fn should_use_chunked(&self, size: u64) -> bool;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
//! keeping the application and interface layers independent of specific
|
||||
//! compression implementations (gzip, zstd, etc.).
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// Compression level settings for file compression operations.
|
||||
///
|
||||
@@ -30,7 +30,11 @@ pub enum CompressionLevel {
|
||||
#[async_trait]
|
||||
pub trait CompressionPort: Send + Sync + 'static {
|
||||
/// 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.
|
||||
async fn decompress_data(&self, compressed_data: &[u8]) -> Result<Vec<u8>, DomainError>;
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
//! keeping the application and interface layers independent of the specific
|
||||
//! content-addressable storage implementation.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use crate::common::errors::DomainError;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use serde::Serialize;
|
||||
use crate::common::errors::DomainError;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Metadata of a stored blob in the dedup system.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::Result;
|
||||
use crate::application::dtos::favorites_dto::FavoriteItemDto;
|
||||
use crate::common::errors::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// Defines operations for managing user favorites
|
||||
#[async_trait]
|
||||
@@ -12,7 +12,12 @@ pub trait FavoritesUseCase: Send + Sync {
|
||||
async fn add_to_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
|
||||
|
||||
/// 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
|
||||
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
use std::pin::Pin;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::common::errors::DomainError;
|
||||
@@ -48,7 +48,13 @@ pub trait FileUploadUseCase: Send + Sync + 'static {
|
||||
) -> Result<(FileDto, UploadStrategy), DomainError>;
|
||||
|
||||
/// 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)
|
||||
async fn update_file(&self, path: &str, content: &[u8]) -> Result<(), DomainError>;
|
||||
@@ -92,7 +98,10 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
||||
|
||||
/// 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.
|
||||
///
|
||||
@@ -123,7 +132,11 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
||||
#[async_trait]
|
||||
pub trait FileManagementUseCase: Send + Sync + 'static {
|
||||
/// 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
|
||||
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<FileDto, DomainError>;
|
||||
@@ -138,11 +151,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
|
||||
/// 3. Decrements the dedup reference count for the content hash.
|
||||
///
|
||||
/// Returns `Ok(true)` when trashed, `Ok(false)` when permanently deleted.
|
||||
async fn delete_with_cleanup(
|
||||
&self,
|
||||
id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<bool, DomainError>;
|
||||
async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result<bool, DomainError>;
|
||||
}
|
||||
|
||||
/// Factory for creating file use case implementations
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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::common::errors::DomainError;
|
||||
|
||||
@@ -23,11 +25,12 @@ pub trait FolderUseCase: Send + Sync + 'static {
|
||||
async fn list_folders_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto
|
||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
|
||||
|
||||
/// 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
|
||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError>;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::path::PathBuf;
|
||||
use async_trait::async_trait;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
|
||||
// Re-export domain repository traits for backward compatibility
|
||||
pub use crate::domain::repositories::folder_repository::FolderRepository;
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::Result;
|
||||
use crate::application::dtos::recent_dto::RecentItemDto;
|
||||
use crate::common::errors::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// Defines operations for managing user recent items
|
||||
#[async_trait]
|
||||
pub trait RecentItemsUseCase: Send + Sync {
|
||||
/// 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
|
||||
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
|
||||
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
|
||||
async fn clear_recent_items(&self, user_id: &str) -> Result<()>;
|
||||
|
||||
@@ -3,13 +3,12 @@ use async_trait::async_trait;
|
||||
use crate::{
|
||||
application::dtos::{
|
||||
pagination::PaginatedResponseDto,
|
||||
share_dto::{CreateShareDto, ShareDto, UpdateShareDto}
|
||||
share_dto::{CreateShareDto, ShareDto, UpdateShareDto},
|
||||
},
|
||||
common::errors::DomainError,
|
||||
domain::entities::share::ShareItemType,
|
||||
};
|
||||
|
||||
|
||||
#[async_trait]
|
||||
pub trait ShareUseCase: Send + Sync + 'static {
|
||||
/// Create a new shared link for a file or folder
|
||||
@@ -63,23 +62,38 @@ pub trait ShareUseCase: Send + Sync + 'static {
|
||||
|
||||
#[async_trait]
|
||||
pub trait ShareStoragePort: Send + Sync + 'static {
|
||||
async fn save_share(&self, share: &crate::domain::entities::share::Share)
|
||||
-> Result<crate::domain::entities::share::Share, DomainError>;
|
||||
async fn save_share(
|
||||
&self,
|
||||
share: &crate::domain::entities::share::Share,
|
||||
) -> Result<crate::domain::entities::share::Share, DomainError>;
|
||||
|
||||
async fn find_share_by_id(&self, id: &str)
|
||||
-> Result<crate::domain::entities::share::Share, DomainError>;
|
||||
async fn find_share_by_id(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<crate::domain::entities::share::Share, DomainError>;
|
||||
|
||||
async fn find_share_by_token(&self, token: &str)
|
||||
-> Result<crate::domain::entities::share::Share, DomainError>;
|
||||
async fn find_share_by_token(
|
||||
&self,
|
||||
token: &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_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 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 find_shares_by_user(&self, user_id: &str, offset: usize, limit: usize)
|
||||
-> Result<(Vec<crate::domain::entities::share::Share>, usize), DomainError>;
|
||||
async fn find_shares_by_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> Result<(Vec<crate::domain::entities::share::Share>, usize), DomainError>;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use std::path::PathBuf;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use serde_json::Value;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
// Re-export domain repository traits for backward compatibility.
|
||||
// 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;
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
@@ -92,17 +94,14 @@ pub trait FileWritePort: Send + Sync + 'static {
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Renames a file (same folder, different name).
|
||||
async fn rename_file(
|
||||
&self,
|
||||
file_id: &str,
|
||||
new_name: &str,
|
||||
) -> Result<File, DomainError>;
|
||||
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<File, DomainError>;
|
||||
|
||||
/// Deletes a file.
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// 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).
|
||||
///
|
||||
@@ -122,7 +121,11 @@ pub trait FileWritePort: Send + Sync + 'static {
|
||||
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// 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)
|
||||
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
//! keeping the application and interface layers independent of specific
|
||||
//! image processing implementations.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use crate::common::errors::DomainError;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use crate::common::errors::DomainError;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Thumbnail sizes supported by the system.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@@ -42,7 +42,11 @@ impl ThumbnailSize {
|
||||
|
||||
/// Get all thumbnail sizes.
|
||||
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.
|
||||
///
|
||||
/// Called after file upload to pre-generate thumbnails.
|
||||
fn generate_all_sizes_background(
|
||||
self: Arc<Self>,
|
||||
file_id: String,
|
||||
original_path: PathBuf,
|
||||
);
|
||||
fn generate_all_sizes_background(self: Arc<Self>, file_id: String, original_path: PathBuf);
|
||||
|
||||
/// Delete all thumbnails for a file.
|
||||
async fn delete_thumbnails(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
//! (e.g., JPEG/PNG → WebP), keeping the application and interface layers
|
||||
//! independent of specific image processing implementations.
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Supported output formats for image transcoding.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
//! keeping the interface layer independent of specific ZIP
|
||||
//! implementation details.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// Port for ZIP archive operations.
|
||||
///
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::domain::repositories::settings_repository::SettingsRepository;
|
||||
use crate::application::services::auth_application_service::AuthApplicationService;
|
||||
use crate::application::dtos::settings_dto::{
|
||||
OidcSettingsDto, SaveOidcSettingsDto, OidcTestResultDto, TestOidcConnectionDto,
|
||||
OidcSettingsDto, OidcTestResultDto, SaveOidcSettingsDto, TestOidcConnectionDto,
|
||||
};
|
||||
use crate::infrastructure::services::oidc_service::OidcService;
|
||||
use crate::application::services::auth_application_service::AuthApplicationService;
|
||||
use crate::common::config::OidcConfig;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::domain::repositories::settings_repository::SettingsRepository;
|
||||
use crate::infrastructure::services::oidc_service::OidcService;
|
||||
|
||||
/// Admin settings service — manages platform configuration in the database.
|
||||
///
|
||||
@@ -52,7 +52,10 @@ impl AdminSettingsService {
|
||||
("OXICLOUD_OIDC_SCOPES", "scopes"),
|
||||
("OXICLOUD_OIDC_AUTO_PROVISION", "auto_provision"),
|
||||
("OXICLOUD_OIDC_ADMIN_GROUPS", "admin_groups"),
|
||||
("OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN", "disable_password_login"),
|
||||
(
|
||||
"OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN",
|
||||
"disable_password_login",
|
||||
),
|
||||
("OXICLOUD_OIDC_PROVIDER_NAME", "provider_name"),
|
||||
];
|
||||
for (env_key, field_name) in &vars {
|
||||
@@ -66,17 +69,39 @@ impl AdminSettingsService {
|
||||
/// Apply environment variable overrides on top of a config
|
||||
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; }
|
||||
if std::env::var("OXICLOUD_OIDC_ISSUER_URL").is_ok() { config.issuer_url = e.issuer_url.clone(); }
|
||||
if std::env::var("OXICLOUD_OIDC_CLIENT_ID").is_ok() { config.client_id = e.client_id.clone(); }
|
||||
if std::env::var("OXICLOUD_OIDC_CLIENT_SECRET").is_ok() { config.client_secret = e.client_secret.clone(); }
|
||||
if std::env::var("OXICLOUD_OIDC_SCOPES").is_ok() { config.scopes = e.scopes.clone(); }
|
||||
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_AUTO_PROVISION").is_ok() { config.auto_provision = e.auto_provision; }
|
||||
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_PROVIDER_NAME").is_ok() { config.provider_name = e.provider_name.clone(); }
|
||||
if std::env::var("OXICLOUD_OIDC_ENABLED").is_ok() {
|
||||
config.enabled = e.enabled;
|
||||
}
|
||||
if std::env::var("OXICLOUD_OIDC_ISSUER_URL").is_ok() {
|
||||
config.issuer_url = e.issuer_url.clone();
|
||||
}
|
||||
if std::env::var("OXICLOUD_OIDC_CLIENT_ID").is_ok() {
|
||||
config.client_id = e.client_id.clone();
|
||||
}
|
||||
if std::env::var("OXICLOUD_OIDC_CLIENT_SECRET").is_ok() {
|
||||
config.client_secret = e.client_secret.clone();
|
||||
}
|
||||
if std::env::var("OXICLOUD_OIDC_SCOPES").is_ok() {
|
||||
config.scopes = e.scopes.clone();
|
||||
}
|
||||
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_AUTO_PROVISION").is_ok() {
|
||||
config.auto_provision = e.auto_provision;
|
||||
}
|
||||
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_PROVIDER_NAME").is_ok() {
|
||||
config.provider_name = e.provider_name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the effective OIDC config: DB settings + env var overrides + defaults.
|
||||
@@ -85,17 +110,35 @@ impl AdminSettingsService {
|
||||
let d = OidcConfig::default();
|
||||
|
||||
let mut config = OidcConfig {
|
||||
enabled: db.get("oidc.enabled").and_then(|v| v.parse().ok()).unwrap_or(d.enabled),
|
||||
enabled: db
|
||||
.get("oidc.enabled")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(d.enabled),
|
||||
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),
|
||||
client_secret: db.get("oidc.client_secret").cloned().unwrap_or(d.client_secret),
|
||||
client_secret: db
|
||||
.get("oidc.client_secret")
|
||||
.cloned()
|
||||
.unwrap_or(d.client_secret),
|
||||
redirect_uri: self.callback_url(),
|
||||
scopes: db.get("oidc.scopes").cloned().unwrap_or(d.scopes),
|
||||
frontend_url: self.server_base_url.clone(),
|
||||
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),
|
||||
disable_password_login: db.get("oidc.disable_password_login").and_then(|v| v.parse().ok()).unwrap_or(d.disable_password_login),
|
||||
provider_name: db.get("oidc.provider_name").cloned().unwrap_or(d.provider_name),
|
||||
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),
|
||||
disable_password_login: db
|
||||
.get("oidc.disable_password_login")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(d.disable_password_login),
|
||||
provider_name: db
|
||||
.get("oidc.provider_name")
|
||||
.cloned()
|
||||
.unwrap_or(d.provider_name),
|
||||
};
|
||||
|
||||
// Env vars override DB
|
||||
@@ -108,19 +151,36 @@ impl AdminSettingsService {
|
||||
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 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);
|
||||
|
||||
Ok(OidcSettingsDto {
|
||||
enabled: db.get("oidc.enabled").and_then(|v| v.parse().ok()).unwrap_or(d.enabled),
|
||||
enabled: db
|
||||
.get("oidc.enabled")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(d.enabled),
|
||||
issuer_url: db.get("oidc.issuer_url").cloned().unwrap_or_default(),
|
||||
client_id: db.get("oidc.client_id").cloned().unwrap_or_default(),
|
||||
client_secret_set: has_secret,
|
||||
scopes: db.get("oidc.scopes").cloned().unwrap_or(d.scopes),
|
||||
auto_provision: db.get("oidc.auto_provision").and_then(|v| v.parse().ok()).unwrap_or(d.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_default(),
|
||||
disable_password_login: db.get("oidc.disable_password_login").and_then(|v| v.parse().ok()).unwrap_or(d.disable_password_login),
|
||||
provider_name: db.get("oidc.provider_name").cloned().unwrap_or(d.provider_name),
|
||||
disable_password_login: db
|
||||
.get("oidc.disable_password_login")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(d.disable_password_login),
|
||||
provider_name: db
|
||||
.get("oidc.provider_name")
|
||||
.cloned()
|
||||
.unwrap_or(d.provider_name),
|
||||
callback_url: self.callback_url(),
|
||||
env_overrides: self.get_env_overrides(),
|
||||
})
|
||||
@@ -135,34 +195,61 @@ impl AdminSettingsService {
|
||||
let cat = "oidc";
|
||||
let by = Some(updated_by);
|
||||
|
||||
self.settings_repo.set("oidc.enabled", &dto.enabled.to_string(), cat, false, by).await?;
|
||||
self.settings_repo.set("oidc.issuer_url", &dto.issuer_url, cat, false, by).await?;
|
||||
self.settings_repo.set("oidc.client_id", &dto.client_id, cat, false, by).await?;
|
||||
self.settings_repo
|
||||
.set("oidc.enabled", &dto.enabled.to_string(), cat, false, by)
|
||||
.await?;
|
||||
self.settings_repo
|
||||
.set("oidc.issuer_url", &dto.issuer_url, cat, false, by)
|
||||
.await?;
|
||||
self.settings_repo
|
||||
.set("oidc.client_id", &dto.client_id, cat, false, by)
|
||||
.await?;
|
||||
|
||||
if let Some(ref secret) = dto.client_secret
|
||||
&& !secret.is_empty() {
|
||||
self.settings_repo.set("oidc.client_secret", secret, cat, true, by).await?;
|
||||
&& !secret.is_empty()
|
||||
{
|
||||
self.settings_repo
|
||||
.set("oidc.client_secret", secret, cat, true, by)
|
||||
.await?;
|
||||
}
|
||||
if let Some(ref v) = dto.scopes {
|
||||
self.settings_repo.set("oidc.scopes", v, cat, false, by).await?;
|
||||
self.settings_repo
|
||||
.set("oidc.scopes", v, cat, false, by)
|
||||
.await?;
|
||||
}
|
||||
if let Some(v) = dto.auto_provision {
|
||||
self.settings_repo.set("oidc.auto_provision", &v.to_string(), cat, false, by).await?;
|
||||
self.settings_repo
|
||||
.set("oidc.auto_provision", &v.to_string(), cat, false, by)
|
||||
.await?;
|
||||
}
|
||||
if let Some(ref v) = dto.admin_groups {
|
||||
self.settings_repo.set("oidc.admin_groups", v, cat, false, by).await?;
|
||||
self.settings_repo
|
||||
.set("oidc.admin_groups", v, cat, false, by)
|
||||
.await?;
|
||||
}
|
||||
if let Some(v) = dto.disable_password_login {
|
||||
self.settings_repo.set("oidc.disable_password_login", &v.to_string(), cat, false, by).await?;
|
||||
self.settings_repo
|
||||
.set(
|
||||
"oidc.disable_password_login",
|
||||
&v.to_string(),
|
||||
cat,
|
||||
false,
|
||||
by,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if let Some(ref v) = dto.provider_name {
|
||||
self.settings_repo.set("oidc.provider_name", v, cat, false, by).await?;
|
||||
self.settings_repo
|
||||
.set("oidc.provider_name", v, cat, false, by)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Hot-reload OIDC service
|
||||
let eff = self.load_effective_oidc_config().await?;
|
||||
if eff.enabled && !eff.issuer_url.is_empty()
|
||||
&& !eff.client_id.is_empty() && !eff.client_secret.is_empty()
|
||||
if eff.enabled
|
||||
&& !eff.issuer_url.is_empty()
|
||||
&& !eff.client_id.is_empty()
|
||||
&& !eff.client_secret.is_empty()
|
||||
{
|
||||
let svc = Arc::new(OidcService::new(eff.clone()));
|
||||
self.auth_app_service.reload_oidc(svc, eff);
|
||||
@@ -186,16 +273,23 @@ impl AdminSettingsService {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError, "OIDC", format!("HTTP client error: {}", e),
|
||||
))?;
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"OIDC",
|
||||
format!("HTTP client error: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let resp = match client.get(&discovery_url).send().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Ok(OidcTestResultDto {
|
||||
success: false,
|
||||
message: format!("Cannot reach the OIDC provider: {}. Check your Issuer URL.", e),
|
||||
message: format!(
|
||||
"Cannot reach the OIDC provider: {}. Check your Issuer URL.",
|
||||
e
|
||||
),
|
||||
issuer: None,
|
||||
authorization_endpoint: None,
|
||||
token_endpoint: None,
|
||||
@@ -291,12 +385,14 @@ impl AdminSettingsService {
|
||||
enabled: bool,
|
||||
updated_by: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
self.settings_repo.set(
|
||||
self.settings_repo
|
||||
.set(
|
||||
"registration_enabled",
|
||||
if enabled { "true" } else { "false" },
|
||||
"general",
|
||||
false,
|
||||
Some(updated_by),
|
||||
).await
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
use std::path::PathBuf;
|
||||
use crate::domain::entities::user::{User, UserRole};
|
||||
use crate::domain::entities::session::Session;
|
||||
use crate::application::ports::auth_ports::{UserStoragePort, SessionStoragePort, PasswordHasherPort, TokenServicePort, OidcServicePort, OidcIdClaims};
|
||||
use crate::application::dtos::user_dto::{UserDto, RegisterDto, LoginDto, AuthResponseDto, ChangePasswordDto, RefreshTokenDto};
|
||||
use crate::application::dtos::folder_dto::CreateFolderDto;
|
||||
use crate::application::dtos::user_dto::{
|
||||
AuthResponseDto, ChangePasswordDto, LoginDto, RefreshTokenDto, RegisterDto, UserDto,
|
||||
};
|
||||
use crate::application::ports::auth_ports::{
|
||||
OidcIdClaims, OidcServicePort, PasswordHasherPort, SessionStoragePort, TokenServicePort,
|
||||
UserStoragePort,
|
||||
};
|
||||
use crate::application::ports::inbound::FolderUseCase;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::common::config::OidcConfig;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::domain::entities::session::Session;
|
||||
use crate::domain::entities::user::{User, UserRole};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::RwLock;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Maximum age for pending OIDC flows (10 minutes)
|
||||
const OIDC_FLOW_TTL_SECS: u64 = 600;
|
||||
@@ -71,7 +76,10 @@ impl AuthApplicationService {
|
||||
token_service,
|
||||
folder_service: None,
|
||||
storage_path,
|
||||
oidc: RwLock::new(OidcState { service: None, config: None }),
|
||||
oidc: RwLock::new(OidcState {
|
||||
service: None,
|
||||
config: None,
|
||||
}),
|
||||
pending_oidc_flows: Mutex::new(HashMap::new()),
|
||||
pending_oidc_tokens: Mutex::new(HashMap::new()),
|
||||
}
|
||||
@@ -92,7 +100,11 @@ impl AuthApplicationService {
|
||||
tracing::info!(
|
||||
"Available disk space ({} bytes) is less than default {} quota ({} bytes) — capping quota",
|
||||
avail_i64,
|
||||
if *role == UserRole::Admin { "admin" } else { "user" },
|
||||
if *role == UserRole::Admin {
|
||||
"admin"
|
||||
} else {
|
||||
"user"
|
||||
},
|
||||
base_quota,
|
||||
);
|
||||
avail_i64
|
||||
@@ -126,7 +138,11 @@ impl AuthApplicationService {
|
||||
}
|
||||
|
||||
/// Configures the OIDC service
|
||||
pub fn with_oidc(self, oidc_service: Arc<dyn OidcServicePort>, oidc_config: OidcConfig) -> Self {
|
||||
pub fn with_oidc(
|
||||
self,
|
||||
oidc_service: Arc<dyn OidcServicePort>,
|
||||
oidc_config: OidcConfig,
|
||||
) -> Self {
|
||||
{
|
||||
let mut state = self.oidc.write().unwrap();
|
||||
state.service = Some(oidc_service);
|
||||
@@ -158,7 +174,10 @@ impl AuthApplicationService {
|
||||
/// Returns whether password login is disabled (OIDC-only mode)
|
||||
pub fn password_login_disabled(&self) -> bool {
|
||||
let state = self.oidc.read().unwrap();
|
||||
state.config.as_ref().is_some_and(|c| c.disable_password_login)
|
||||
state
|
||||
.config
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.disable_password_login)
|
||||
}
|
||||
|
||||
/// Returns a clone of the OIDC config if available
|
||||
@@ -175,25 +194,35 @@ impl AuthApplicationService {
|
||||
|
||||
pub async fn register(&self, dto: RegisterDto) -> Result<UserDto, DomainError> {
|
||||
// Check for duplicate user
|
||||
if self.user_storage.get_user_by_username(&dto.username).await.is_ok() {
|
||||
if self
|
||||
.user_storage
|
||||
.get_user_by_username(&dto.username)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AlreadyExists,
|
||||
"User",
|
||||
format!("User '{}' already exists", dto.username)
|
||||
format!("User '{}' already exists", dto.username),
|
||||
));
|
||||
}
|
||||
|
||||
if self.user_storage.get_user_by_email(&dto.email).await.is_ok() {
|
||||
if self
|
||||
.user_storage
|
||||
.get_user_by_email(&dto.email)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AlreadyExists,
|
||||
"User",
|
||||
format!("Email '{}' is already registered", dto.email)
|
||||
format!("Email '{}' is already registered", dto.email),
|
||||
));
|
||||
}
|
||||
|
||||
// Check if the user wants to create an admin
|
||||
let is_admin_request = dto.username.to_lowercase() == "admin" ||
|
||||
(dto.role.is_some() && dto.role.as_ref().unwrap().to_lowercase() == "admin");
|
||||
let is_admin_request = dto.username.to_lowercase() == "admin"
|
||||
|| (dto.role.is_some() && dto.role.as_ref().unwrap().to_lowercase() == "admin");
|
||||
|
||||
// If trying to create an admin, check if admins already exist in the system
|
||||
if is_admin_request {
|
||||
@@ -207,29 +236,37 @@ impl AuthApplicationService {
|
||||
Ok(user_count) => {
|
||||
// If there are more than 2 users (admin + test), it is not a clean install
|
||||
if user_count > 2 {
|
||||
tracing::warn!("Attempt to create additional admin rejected: at least one admin already exists");
|
||||
tracing::warn!(
|
||||
"Attempt to create additional admin rejected: at least one admin already exists"
|
||||
);
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"User",
|
||||
"Creating additional admin users from the registration page is not allowed"
|
||||
"Creating additional admin users from the registration page is not allowed",
|
||||
));
|
||||
}
|
||||
// Otherwise, it is a clean install and the first admin is allowed
|
||||
tracing::info!("Allowing admin creation on clean install");
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// Cannot verify user count — treat as bootstrap scenario
|
||||
tracing::warn!("Could not count users ({}). Allowing admin creation for bootstrap.", e);
|
||||
tracing::warn!(
|
||||
"Could not count users ({}). Allowing admin creation for bootstrap.",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
// Any DB error (table missing, connection issue, etc.) means we
|
||||
// cannot verify admin state. Allow admin creation so the user can
|
||||
// bootstrap the system. If the DB is truly broken the INSERT will
|
||||
// fail anyway with a clear error.
|
||||
tracing::warn!("Could not count admin users ({}). Allowing admin creation for bootstrap.", e);
|
||||
tracing::warn!(
|
||||
"Could not count admin users ({}). Allowing admin creation for bootstrap.",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,7 +296,7 @@ impl AuthApplicationService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"User",
|
||||
"Password must be at least 8 characters long"
|
||||
"Password must be at least 8 characters long",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -267,17 +304,15 @@ impl AuthApplicationService {
|
||||
let password_hash = self.password_hasher.hash_password(&dto.password)?;
|
||||
|
||||
// Create user with the pre-generated hash
|
||||
let user = User::new(
|
||||
dto.username.clone(),
|
||||
dto.email,
|
||||
password_hash,
|
||||
role,
|
||||
quota,
|
||||
).map_err(|e| DomainError::new(
|
||||
let user = User::new(dto.username.clone(), dto.email, password_hash, role, quota).map_err(
|
||||
|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"User",
|
||||
format!("Error creating user: {}", e)
|
||||
))?;
|
||||
format!("Error creating user: {}", e),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
// Save user
|
||||
let created_user = self.user_storage.create_user(user).await?;
|
||||
@@ -286,10 +321,13 @@ impl AuthApplicationService {
|
||||
if let Some(folder_service) = &self.folder_service {
|
||||
let folder_name = format!("My Folder - {}", dto.username);
|
||||
|
||||
match folder_service.create_folder(CreateFolderDto {
|
||||
match folder_service
|
||||
.create_folder(CreateFolderDto {
|
||||
name: folder_name,
|
||||
parent_id: None,
|
||||
}).await {
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(folder) => {
|
||||
tracing::info!(
|
||||
"Personal folder created for user {}: {} (ID: {})",
|
||||
@@ -300,7 +338,7 @@ impl AuthApplicationService {
|
||||
|
||||
// Here we could save the folder-to-user association,
|
||||
// for example, in a folder-user relationship table
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// We don't fail registration due to a folder creation error,
|
||||
// but we log it for investigation
|
||||
@@ -324,32 +362,33 @@ impl AuthApplicationService {
|
||||
|
||||
pub async fn login(&self, dto: LoginDto) -> Result<AuthResponseDto, DomainError> {
|
||||
// Find user
|
||||
let mut user = self.user_storage
|
||||
let mut user = self
|
||||
.user_storage
|
||||
.get_user_by_username(&dto.username)
|
||||
.await
|
||||
.map_err(|_| DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Auth",
|
||||
"Invalid credentials"
|
||||
))?;
|
||||
.map_err(|_| {
|
||||
DomainError::new(ErrorKind::AccessDenied, "Auth", "Invalid credentials")
|
||||
})?;
|
||||
|
||||
// Check if user is active
|
||||
if !user.is_active() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Auth",
|
||||
"Account deactivated"
|
||||
"Account deactivated",
|
||||
));
|
||||
}
|
||||
|
||||
// Verify password using the injected hasher
|
||||
let is_valid = self.password_hasher.verify_password(&dto.password, user.password_hash())?;
|
||||
let is_valid = self
|
||||
.password_hasher
|
||||
.verify_password(&dto.password, user.password_hash())?;
|
||||
|
||||
if !is_valid {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Auth",
|
||||
"Invalid credentials"
|
||||
"Invalid credentials",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -383,9 +422,13 @@ impl AuthApplicationService {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn refresh_token(&self, dto: RefreshTokenDto) -> Result<AuthResponseDto, DomainError> {
|
||||
pub async fn refresh_token(
|
||||
&self,
|
||||
dto: RefreshTokenDto,
|
||||
) -> Result<AuthResponseDto, DomainError> {
|
||||
// Get valid session
|
||||
let session = self.session_storage
|
||||
let session = self
|
||||
.session_storage
|
||||
.get_session_by_refresh_token(&dto.refresh_token)
|
||||
.await?;
|
||||
|
||||
@@ -394,21 +437,19 @@ impl AuthApplicationService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Auth",
|
||||
"Session expired or invalid"
|
||||
"Session expired or invalid",
|
||||
));
|
||||
}
|
||||
|
||||
// Get user
|
||||
let user = self.user_storage
|
||||
.get_user_by_id(session.user_id())
|
||||
.await?;
|
||||
let user = self.user_storage.get_user_by_id(session.user_id()).await?;
|
||||
|
||||
// Check if user is active
|
||||
if !user.is_active() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Auth",
|
||||
"Account deactivated"
|
||||
"Account deactivated",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -442,7 +483,11 @@ impl AuthApplicationService {
|
||||
|
||||
pub async fn logout(&self, user_id: &str, refresh_token: &str) -> Result<(), DomainError> {
|
||||
// Get session
|
||||
let session = match self.session_storage.get_session_by_refresh_token(refresh_token).await {
|
||||
let session = match self
|
||||
.session_storage
|
||||
.get_session_by_refresh_token(refresh_token)
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
// If the session doesn't exist, we consider the logout successful
|
||||
Err(_) => return Ok(()),
|
||||
@@ -453,7 +498,7 @@ impl AuthApplicationService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Auth",
|
||||
"The session does not belong to the user"
|
||||
"The session does not belong to the user",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -465,23 +510,32 @@ impl AuthApplicationService {
|
||||
|
||||
pub async fn logout_all(&self, user_id: &str) -> Result<u64, DomainError> {
|
||||
// Revoke all user sessions
|
||||
let revoked_count = self.session_storage.revoke_all_user_sessions(user_id).await?;
|
||||
let revoked_count = self
|
||||
.session_storage
|
||||
.revoke_all_user_sessions(user_id)
|
||||
.await?;
|
||||
|
||||
Ok(revoked_count)
|
||||
}
|
||||
|
||||
pub async fn change_password(&self, user_id: &str, dto: ChangePasswordDto) -> Result<(), DomainError> {
|
||||
pub async fn change_password(
|
||||
&self,
|
||||
user_id: &str,
|
||||
dto: ChangePasswordDto,
|
||||
) -> Result<(), DomainError> {
|
||||
// Get user
|
||||
let mut user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
|
||||
// Verify current password using the injected hasher
|
||||
let is_valid = self.password_hasher.verify_password(&dto.current_password, user.password_hash())?;
|
||||
let is_valid = self
|
||||
.password_hasher
|
||||
.verify_password(&dto.current_password, user.password_hash())?;
|
||||
|
||||
if !is_valid {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Auth",
|
||||
"Current password is incorrect"
|
||||
"Current password is incorrect",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -490,7 +544,7 @@ impl AuthApplicationService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"User",
|
||||
"Password must be at least 8 characters long"
|
||||
"Password must be at least 8 characters long",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -502,7 +556,9 @@ impl AuthApplicationService {
|
||||
self.user_storage.update_user(user).await?;
|
||||
|
||||
// Optional: revoke all sessions to force re-login with new password
|
||||
self.session_storage.revoke_all_user_sessions(user_id).await?;
|
||||
self.session_storage
|
||||
.revoke_all_user_sessions(user_id)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -528,12 +584,17 @@ impl AuthApplicationService {
|
||||
pub async fn count_admin_users(&self) -> Result<i64, DomainError> {
|
||||
// Use the list_users_by_role method or similar from user_storage port
|
||||
// For now, we'll use a basic implementation that counts all users with role = "admin"
|
||||
let admin_users = self.user_storage.list_users_by_role("admin").await
|
||||
.map_err(|e| DomainError::new(
|
||||
let admin_users = self
|
||||
.user_storage
|
||||
.list_users_by_role("admin")
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"User",
|
||||
format!("Error counting admin users: {}", e)
|
||||
))?;
|
||||
format!("Error counting admin users: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(admin_users.len() as i64)
|
||||
}
|
||||
@@ -542,12 +603,13 @@ impl AuthApplicationService {
|
||||
// Used to determine if this is a fresh install
|
||||
pub async fn count_all_users(&self) -> Result<i64, DomainError> {
|
||||
// Get all users with large limit and 0 offset
|
||||
let all_users = self.user_storage.list_users(1000, 0).await
|
||||
.map_err(|e| DomainError::new(
|
||||
let all_users = self.user_storage.list_users(1000, 0).await.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"User",
|
||||
format!("Error counting users: {}", e)
|
||||
))?;
|
||||
format!("Error counting users: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(all_users.len() as i64)
|
||||
}
|
||||
@@ -559,13 +621,17 @@ impl AuthApplicationService {
|
||||
match self.get_user_by_username("admin").await {
|
||||
Ok(default_admin) => {
|
||||
// Delete the default admin user
|
||||
self.user_storage.delete_user(&default_admin.id).await
|
||||
.map_err(|e| DomainError::new(
|
||||
self.user_storage
|
||||
.delete_user(&default_admin.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"User",
|
||||
format!("Error deleting default admin user: {}", e)
|
||||
))
|
||||
},
|
||||
format!("Error deleting default admin user: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
Err(_) => {
|
||||
// Admin user doesn't exist, nothing to do
|
||||
tracing::info!("Default admin user not found, nothing to delete");
|
||||
@@ -581,12 +647,16 @@ impl AuthApplicationService {
|
||||
let default_admin = self.get_user_by_username("admin").await?;
|
||||
|
||||
// 2. Delete the default admin user
|
||||
self.user_storage.delete_user(&default_admin.id).await
|
||||
.map_err(|e| DomainError::new(
|
||||
self.user_storage
|
||||
.delete_user(&default_admin.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"User",
|
||||
format!("Error deleting default admin user: {}", e)
|
||||
))?;
|
||||
format!("Error deleting default admin user: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// 3. Create new admin user with the provided credentials but admin role
|
||||
let admin_role = UserRole::Admin;
|
||||
@@ -601,11 +671,14 @@ impl AuthApplicationService {
|
||||
dto.password.clone(),
|
||||
admin_role,
|
||||
admin_quota,
|
||||
).map_err(|e| DomainError::new(
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"User",
|
||||
format!("Error creating admin user: {}", e)
|
||||
))?;
|
||||
format!("Error creating admin user: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// 4. Save the new admin user
|
||||
let created_user = self.user_storage.create_user(user).await?;
|
||||
@@ -614,10 +687,13 @@ impl AuthApplicationService {
|
||||
if let Some(folder_service) = &self.folder_service {
|
||||
let folder_name = format!("My Folder - {}", dto.username);
|
||||
|
||||
match folder_service.create_folder(CreateFolderDto {
|
||||
match folder_service
|
||||
.create_folder(CreateFolderDto {
|
||||
name: folder_name,
|
||||
parent_id: None,
|
||||
}).await {
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(folder) => {
|
||||
tracing::info!(
|
||||
"Personal folder created for admin {}: {} (ID: {})",
|
||||
@@ -625,7 +701,7 @@ impl AuthApplicationService {
|
||||
folder.name,
|
||||
folder.id
|
||||
);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Could not create personal folder for admin {}: {}",
|
||||
@@ -657,28 +733,37 @@ impl AuthApplicationService {
|
||||
// Validate username length
|
||||
if dto.username.len() < 3 || dto.username.len() > 32 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput, "User",
|
||||
ErrorKind::InvalidInput,
|
||||
"User",
|
||||
"Username must be between 3 and 32 characters".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check for duplicate username
|
||||
if self.user_storage.get_user_by_username(&dto.username).await.is_ok() {
|
||||
if self
|
||||
.user_storage
|
||||
.get_user_by_username(&dto.username)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AlreadyExists, "User",
|
||||
ErrorKind::AlreadyExists,
|
||||
"User",
|
||||
format!("User '{}' already exists", dto.username),
|
||||
));
|
||||
}
|
||||
|
||||
// Email: use provided or generate placeholder
|
||||
let email = dto.email
|
||||
let email = dto
|
||||
.email
|
||||
.filter(|e| !e.trim().is_empty())
|
||||
.unwrap_or_else(|| format!("{}@oxicloud.local", dto.username));
|
||||
|
||||
// Check email uniqueness
|
||||
if self.user_storage.get_user_by_email(&email).await.is_ok() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AlreadyExists, "User",
|
||||
ErrorKind::AlreadyExists,
|
||||
"User",
|
||||
format!("Email '{}' is already registered", email),
|
||||
));
|
||||
}
|
||||
@@ -686,7 +771,8 @@ impl AuthApplicationService {
|
||||
// Validate password
|
||||
if dto.password.len() < 8 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput, "User",
|
||||
ErrorKind::InvalidInput,
|
||||
"User",
|
||||
"Password must be at least 8 characters long".to_string(),
|
||||
));
|
||||
}
|
||||
@@ -699,49 +785,59 @@ impl AuthApplicationService {
|
||||
|
||||
// Determine quota
|
||||
let quota = dto.quota_bytes.unwrap_or_else(|| {
|
||||
if role == UserRole::Admin { 107_374_182_400 } else { 1_073_741_824 }
|
||||
if role == UserRole::Admin {
|
||||
107_374_182_400
|
||||
} else {
|
||||
1_073_741_824
|
||||
}
|
||||
});
|
||||
|
||||
// Hash password
|
||||
let password_hash = self.password_hasher.hash_password(&dto.password)?;
|
||||
|
||||
// Create domain entity
|
||||
let user = User::new(
|
||||
dto.username.clone(),
|
||||
email,
|
||||
password_hash,
|
||||
role,
|
||||
quota,
|
||||
).map_err(|e| DomainError::new(
|
||||
ErrorKind::InvalidInput, "User",
|
||||
let user =
|
||||
User::new(dto.username.clone(), email, password_hash, role, quota).map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"User",
|
||||
format!("Error creating user: {}", e),
|
||||
))?;
|
||||
)
|
||||
})?;
|
||||
|
||||
// Persist
|
||||
let created = self.user_storage.create_user(user).await?;
|
||||
|
||||
// Deactivate if requested (User::new always sets active=true)
|
||||
if let Some(false) = dto.active {
|
||||
self.user_storage.set_user_active_status(created.id(), false).await?;
|
||||
self.user_storage
|
||||
.set_user_active_status(created.id(), false)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Create personal folder
|
||||
if let Some(folder_service) = &self.folder_service {
|
||||
let folder_name = format!("My Folder - {}", dto.username);
|
||||
match folder_service.create_folder(CreateFolderDto {
|
||||
match folder_service
|
||||
.create_folder(CreateFolderDto {
|
||||
name: folder_name,
|
||||
parent_id: None,
|
||||
}).await {
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(folder) => {
|
||||
tracing::info!(
|
||||
"Personal folder created for admin-created user {}: {} (ID: {})",
|
||||
created.id(), folder.name, folder.id
|
||||
created.id(),
|
||||
folder.name,
|
||||
folder.id
|
||||
);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Could not create personal folder for user {}: {}",
|
||||
created.id(), e
|
||||
created.id(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -759,7 +855,8 @@ impl AuthApplicationService {
|
||||
) -> Result<(), DomainError> {
|
||||
if new_password.len() < 8 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput, "User",
|
||||
ErrorKind::InvalidInput,
|
||||
"User",
|
||||
"Password must be at least 8 characters long".to_string(),
|
||||
));
|
||||
}
|
||||
@@ -783,7 +880,9 @@ impl AuthApplicationService {
|
||||
|
||||
/// Activate or deactivate a user (admin only)
|
||||
pub async fn set_user_active(&self, user_id: &str, active: bool) -> Result<(), DomainError> {
|
||||
self.user_storage.set_user_active_status(user_id, active).await
|
||||
self.user_storage
|
||||
.set_user_active_status(user_id, active)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Change user role (admin only)
|
||||
@@ -799,7 +898,11 @@ impl AuthApplicationService {
|
||||
}
|
||||
|
||||
/// Update user's storage quota (admin only)
|
||||
pub async fn update_user_quota(&self, user_id: &str, quota_bytes: i64) -> Result<(), DomainError> {
|
||||
pub async fn update_user_quota(
|
||||
&self,
|
||||
user_id: &str,
|
||||
quota_bytes: i64,
|
||||
) -> Result<(), DomainError> {
|
||||
if quota_bytes < 0 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
@@ -807,11 +910,17 @@ impl AuthApplicationService {
|
||||
"Quota must be non-negative".to_string(),
|
||||
));
|
||||
}
|
||||
self.user_storage.update_storage_quota(user_id, quota_bytes).await
|
||||
self.user_storage
|
||||
.update_storage_quota(user_id, quota_bytes)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Check if a user has enough quota for an upload of the given size
|
||||
pub async fn check_quota(&self, user_id: &str, additional_bytes: i64) -> Result<bool, DomainError> {
|
||||
pub async fn check_quota(
|
||||
&self,
|
||||
user_id: &str,
|
||||
additional_bytes: i64,
|
||||
) -> Result<bool, DomainError> {
|
||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
let quota = user.storage_quota_bytes();
|
||||
if quota <= 0 {
|
||||
@@ -833,9 +942,13 @@ impl AuthApplicationService {
|
||||
/// Prepare the OIDC authorization flow: generates CSRF state, PKCE pair,
|
||||
/// nonce, stores them in pending_oidc_flows, and returns the authorize URL.
|
||||
pub async fn prepare_oidc_authorize(&self) -> Result<String, DomainError> {
|
||||
let oidc = self.oidc_service().ok_or_else(|| DomainError::new(
|
||||
ErrorKind::InternalError, "OIDC", "OIDC service not configured",
|
||||
))?;
|
||||
let oidc = self.oidc_service().ok_or_else(|| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"OIDC",
|
||||
"OIDC service not configured",
|
||||
)
|
||||
})?;
|
||||
|
||||
// Generate CSRF state token
|
||||
use rand_core::{OsRng, RngCore};
|
||||
@@ -853,7 +966,7 @@ impl AuthApplicationService {
|
||||
OsRng.fill_bytes(&mut verifier_bytes);
|
||||
let pkce_verifier = base64_url_encode(&verifier_bytes);
|
||||
let pkce_challenge = {
|
||||
use sha2::{Sha256, Digest};
|
||||
use sha2::{Digest, Sha256};
|
||||
let hash = Sha256::digest(pkce_verifier.as_bytes());
|
||||
base64_url_encode(&hash)
|
||||
};
|
||||
@@ -865,17 +978,25 @@ impl AuthApplicationService {
|
||||
let now = Instant::now();
|
||||
flows.retain(|_, f| now.duration_since(f.created_at).as_secs() < OIDC_FLOW_TTL_SECS);
|
||||
|
||||
flows.insert(state_token.clone(), PendingOidcFlow {
|
||||
flows.insert(
|
||||
state_token.clone(),
|
||||
PendingOidcFlow {
|
||||
created_at: now,
|
||||
pkce_verifier,
|
||||
nonce: nonce.clone(),
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Build authorization URL with state, nonce, and PKCE challenge
|
||||
let authorize_url = oidc.get_authorize_url(&state_token, &nonce, &pkce_challenge).await?;
|
||||
let authorize_url = oidc
|
||||
.get_authorize_url(&state_token, &nonce, &pkce_challenge)
|
||||
.await?;
|
||||
|
||||
tracing::info!("OIDC authorize flow prepared (state={}...)", &state_token[..8]);
|
||||
tracing::info!(
|
||||
"OIDC authorize flow prepared (state={}...)",
|
||||
&state_token[..8]
|
||||
);
|
||||
|
||||
Ok(authorize_url)
|
||||
}
|
||||
@@ -899,7 +1020,8 @@ impl AuthApplicationService {
|
||||
if Instant::now().duration_since(flow.created_at).as_secs() >= OIDC_FLOW_TTL_SECS {
|
||||
tracing::warn!("OIDC callback with expired state token");
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied, "OIDC",
|
||||
ErrorKind::AccessDenied,
|
||||
"OIDC",
|
||||
"OIDC authorization flow expired. Please try logging in again.",
|
||||
));
|
||||
}
|
||||
@@ -910,12 +1032,20 @@ impl AuthApplicationService {
|
||||
// Clone the Arc and config out of the RwLock so we don't hold the lock across await points
|
||||
let (oidc, oidc_config) = {
|
||||
let state = self.oidc.read().unwrap();
|
||||
let svc = state.service.clone().ok_or_else(|| DomainError::new(
|
||||
ErrorKind::InternalError, "OIDC", "OIDC service not configured",
|
||||
))?;
|
||||
let cfg = state.config.clone().ok_or_else(|| DomainError::new(
|
||||
ErrorKind::InternalError, "OIDC", "OIDC config not available",
|
||||
))?;
|
||||
let svc = state.service.clone().ok_or_else(|| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"OIDC",
|
||||
"OIDC service not configured",
|
||||
)
|
||||
})?;
|
||||
let cfg = state.config.clone().ok_or_else(|| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"OIDC",
|
||||
"OIDC config not available",
|
||||
)
|
||||
})?;
|
||||
(svc, cfg)
|
||||
};
|
||||
|
||||
@@ -923,7 +1053,9 @@ impl AuthApplicationService {
|
||||
let token_set = oidc.exchange_code(code, &pkce_verifier).await?;
|
||||
|
||||
// 2. Validate ID token and extract claims (with nonce verification)
|
||||
let claims = oidc.validate_id_token(&token_set.id_token, Some(&nonce)).await?;
|
||||
let claims = oidc
|
||||
.validate_id_token(&token_set.id_token, Some(&nonce))
|
||||
.await?;
|
||||
|
||||
// 3. Try to enrich claims from UserInfo endpoint if email is missing
|
||||
let claims = if claims.email.is_none() {
|
||||
@@ -932,11 +1064,18 @@ impl AuthApplicationService {
|
||||
email: user_info.email.or(claims.email),
|
||||
preferred_username: user_info.preferred_username.or(claims.preferred_username),
|
||||
name: user_info.name.or(claims.name),
|
||||
groups: if user_info.groups.is_empty() { claims.groups } else { user_info.groups },
|
||||
groups: if user_info.groups.is_empty() {
|
||||
claims.groups
|
||||
} else {
|
||||
user_info.groups
|
||||
},
|
||||
..claims
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to fetch UserInfo (continuing with ID token claims): {}", e);
|
||||
tracing::warn!(
|
||||
"Failed to fetch UserInfo (continuing with ID token claims): {}",
|
||||
e
|
||||
);
|
||||
claims
|
||||
}
|
||||
}
|
||||
@@ -947,14 +1086,22 @@ impl AuthApplicationService {
|
||||
let provider_name = oidc.provider_name().to_string();
|
||||
|
||||
// 4. Determine username and email
|
||||
let oidc_username = claims.preferred_username.clone()
|
||||
let oidc_username = claims
|
||||
.preferred_username
|
||||
.clone()
|
||||
.or(claims.name.clone())
|
||||
.unwrap_or_else(|| format!("oidc_{}", &claims.sub[..8.min(claims.sub.len())]));
|
||||
let oidc_email = claims.email.clone()
|
||||
let oidc_email = claims
|
||||
.email
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{}@oidc.local", oidc_username));
|
||||
|
||||
// 5. Look up existing user by OIDC subject
|
||||
let user = match self.user_storage.get_user_by_oidc_subject(&provider_name, &claims.sub).await {
|
||||
let user = match self
|
||||
.user_storage
|
||||
.get_user_by_oidc_subject(&provider_name, &claims.sub)
|
||||
.await
|
||||
{
|
||||
Ok(mut existing_user) => {
|
||||
// User exists — update last login
|
||||
existing_user.register_login();
|
||||
@@ -968,15 +1115,20 @@ impl AuthApplicationService {
|
||||
if let Some(_existing) = matched_user {
|
||||
// Email match but no OIDC link — for security, don't auto-link
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AlreadyExists, "OIDC",
|
||||
format!("A user with email '{}' already exists. Contact admin to link your OIDC identity.", oidc_email),
|
||||
ErrorKind::AlreadyExists,
|
||||
"OIDC",
|
||||
format!(
|
||||
"A user with email '{}' already exists. Contact admin to link your OIDC identity.",
|
||||
oidc_email
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// No match — JIT provision if enabled
|
||||
if !oidc_config.auto_provision {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied, "OIDC",
|
||||
ErrorKind::AccessDenied,
|
||||
"OIDC",
|
||||
"Auto-provisioning is disabled. Contact admin to create your account.",
|
||||
));
|
||||
}
|
||||
@@ -993,7 +1145,12 @@ impl AuthApplicationService {
|
||||
}
|
||||
|
||||
// Check for username collision
|
||||
if self.user_storage.get_user_by_username(&username).await.is_ok() {
|
||||
if self
|
||||
.user_storage
|
||||
.get_user_by_username(&username)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
let suffix = &claims.sub[..4.min(claims.sub.len())];
|
||||
username = format!("{}_{}", &username[..username.len().min(27)], suffix);
|
||||
}
|
||||
@@ -1005,18 +1162,27 @@ impl AuthApplicationService {
|
||||
quota,
|
||||
provider_name.clone(),
|
||||
claims.sub.clone(),
|
||||
).map_err(|e| DomainError::new(
|
||||
ErrorKind::InvalidInput, "OIDC",
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"OIDC",
|
||||
format!("Failed to create OIDC user: {}", e),
|
||||
))?;
|
||||
)
|
||||
})?;
|
||||
|
||||
let created_user = self.user_storage.create_user(new_user).await?;
|
||||
|
||||
// Create personal folder
|
||||
self.create_personal_folder(&username, created_user.id()).await;
|
||||
self.create_personal_folder(&username, created_user.id())
|
||||
.await;
|
||||
|
||||
tracing::info!("OIDC user provisioned: {} (provider: {}, sub: {})",
|
||||
created_user.id(), provider_name, claims.sub);
|
||||
tracing::info!(
|
||||
"OIDC user provisioned: {} (provider: {}, sub: {})",
|
||||
created_user.id(),
|
||||
provider_name,
|
||||
claims.sub
|
||||
);
|
||||
|
||||
created_user
|
||||
}
|
||||
@@ -1055,10 +1221,13 @@ impl AuthApplicationService {
|
||||
let now = Instant::now();
|
||||
tokens.retain(|_, t| now.duration_since(t.created_at).as_secs() < OIDC_TOKEN_TTL_SECS);
|
||||
|
||||
tokens.insert(exchange_code.clone(), PendingOidcToken {
|
||||
tokens.insert(
|
||||
exchange_code.clone(),
|
||||
PendingOidcToken {
|
||||
auth_response,
|
||||
created_at: now,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
tracing::info!("OIDC login successful, one-time exchange code generated");
|
||||
@@ -1072,7 +1241,8 @@ impl AuthApplicationService {
|
||||
let mut tokens = self.pending_oidc_tokens.lock().unwrap();
|
||||
let pending = tokens.remove(one_time_code).ok_or_else(|| {
|
||||
DomainError::new(
|
||||
ErrorKind::AccessDenied, "OIDC",
|
||||
ErrorKind::AccessDenied,
|
||||
"OIDC",
|
||||
"Invalid or expired exchange code. Please try logging in again.",
|
||||
)
|
||||
})?;
|
||||
@@ -1080,7 +1250,8 @@ impl AuthApplicationService {
|
||||
// Check TTL
|
||||
if Instant::now().duration_since(pending.created_at).as_secs() >= OIDC_TOKEN_TTL_SECS {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied, "OIDC",
|
||||
ErrorKind::AccessDenied,
|
||||
"OIDC",
|
||||
"Exchange code expired. Please try logging in again.",
|
||||
));
|
||||
}
|
||||
@@ -1106,16 +1277,27 @@ impl AuthApplicationService {
|
||||
async fn create_personal_folder(&self, username: &str, user_id: &str) {
|
||||
if let Some(folder_service) = &self.folder_service {
|
||||
let folder_name = format!("My Folder - {}", username);
|
||||
match folder_service.create_folder(CreateFolderDto {
|
||||
match folder_service
|
||||
.create_folder(CreateFolderDto {
|
||||
name: folder_name.clone(),
|
||||
parent_id: None,
|
||||
}).await {
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(folder) => {
|
||||
tracing::info!("Personal folder created for user {}: {} (ID: {})",
|
||||
user_id, folder.name, folder.id);
|
||||
tracing::info!(
|
||||
"Personal folder created for user {}: {} (ID: {})",
|
||||
user_id,
|
||||
folder.name,
|
||||
folder.id
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create personal folder for user {}: {}", user_id, e);
|
||||
tracing::error!(
|
||||
"Failed to create personal folder for user {}: {}",
|
||||
user_id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use futures::{Future, future::join_all};
|
||||
use std::sync::Arc;
|
||||
use futures::{future::join_all, Future};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Semaphore;
|
||||
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::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
|
||||
#[derive(Debug, Error)]
|
||||
@@ -72,7 +72,7 @@ impl BatchOperationService {
|
||||
file_retrieval: Arc<dyn FileRetrievalUseCase>,
|
||||
file_management: Arc<dyn FileManagementUseCase>,
|
||||
folder_service: Arc<FolderService>,
|
||||
config: AppConfig
|
||||
config: AppConfig,
|
||||
) -> Self {
|
||||
// Limit concurrency based on configuration
|
||||
let max_concurrency = config.concurrency.max_concurrent_files;
|
||||
@@ -90,9 +90,14 @@ impl BatchOperationService {
|
||||
pub fn default(
|
||||
file_retrieval: Arc<dyn FileRetrievalUseCase>,
|
||||
file_management: Arc<dyn FileManagementUseCase>,
|
||||
folder_service: Arc<FolderService>
|
||||
folder_service: Arc<FolderService>,
|
||||
) -> 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
|
||||
@@ -153,14 +158,15 @@ impl BatchOperationService {
|
||||
|
||||
// Complete statistics
|
||||
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);
|
||||
|
||||
info!(
|
||||
"Batch copy completed: {}/{} successful in {}ms",
|
||||
result.stats.successful,
|
||||
result.stats.total,
|
||||
result.stats.execution_time_ms
|
||||
result.stats.successful, result.stats.total, result.stats.execution_time_ms
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
@@ -224,14 +230,15 @@ impl BatchOperationService {
|
||||
|
||||
// Complete statistics
|
||||
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);
|
||||
|
||||
info!(
|
||||
"Batch move completed: {}/{} successful in {}ms",
|
||||
result.stats.successful,
|
||||
result.stats.total,
|
||||
result.stats.execution_time_ms
|
||||
result.stats.successful, result.stats.total, result.stats.execution_time_ms
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
@@ -294,14 +301,15 @@ impl BatchOperationService {
|
||||
|
||||
// Complete statistics
|
||||
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);
|
||||
|
||||
info!(
|
||||
"Batch deletion completed: {}/{} successful in {}ms",
|
||||
result.stats.successful,
|
||||
result.stats.total,
|
||||
result.stats.execution_time_ms
|
||||
result.stats.successful, result.stats.total, result.stats.execution_time_ms
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
@@ -363,14 +371,15 @@ impl BatchOperationService {
|
||||
|
||||
// Complete statistics
|
||||
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);
|
||||
|
||||
info!(
|
||||
"Batch load completed: {}/{} successful in {}ms",
|
||||
result.stats.successful,
|
||||
result.stats.total,
|
||||
result.stats.execution_time_ms
|
||||
result.stats.successful, result.stats.total, result.stats.execution_time_ms
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
@@ -436,14 +445,15 @@ impl BatchOperationService {
|
||||
|
||||
// Complete statistics
|
||||
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);
|
||||
|
||||
info!(
|
||||
"Batch folder deletion completed: {}/{} successful in {}ms",
|
||||
result.stats.successful,
|
||||
result.stats.total,
|
||||
result.stats.execution_time_ms
|
||||
result.stats.successful, result.stats.total, result.stats.execution_time_ms
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
@@ -460,7 +470,10 @@ impl BatchOperationService {
|
||||
F: Fn(T, Arc<Semaphore>) -> Fut + Clone + Send + Sync + '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();
|
||||
|
||||
// Create result structure
|
||||
@@ -508,14 +521,15 @@ impl BatchOperationService {
|
||||
|
||||
// Complete statistics
|
||||
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);
|
||||
|
||||
info!(
|
||||
"Generic batch operation completed: {}/{} successful in {}ms",
|
||||
result.stats.successful,
|
||||
result.stats.total,
|
||||
result.stats.execution_time_ms
|
||||
result.stats.successful, result.stats.total, result.stats.execution_time_ms
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
@@ -550,7 +564,7 @@ impl BatchOperationService {
|
||||
|
||||
let dto = crate::application::dtos::folder_dto::CreateFolderDto {
|
||||
name: name.clone(),
|
||||
parent_id: parent_id.clone()
|
||||
parent_id: parent_id.clone(),
|
||||
};
|
||||
let create_result = folder_service.create_folder(dto).await;
|
||||
|
||||
@@ -582,14 +596,15 @@ impl BatchOperationService {
|
||||
|
||||
// Complete statistics
|
||||
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);
|
||||
|
||||
info!(
|
||||
"Batch folder creation completed: {}/{} successful in {}ms",
|
||||
result.stats.successful,
|
||||
result.stats.total,
|
||||
result.stats.execution_time_ms
|
||||
result.stats.successful, result.stats.total, result.stats.execution_time_ms
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
@@ -651,14 +666,15 @@ impl BatchOperationService {
|
||||
|
||||
// Complete statistics
|
||||
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);
|
||||
|
||||
info!(
|
||||
"Batch folder load completed: {}/{} successful in {}ms",
|
||||
result.stats.successful,
|
||||
result.stats.total,
|
||||
result.stats.execution_time_ms
|
||||
result.stats.successful, result.stats.total, result.stats.execution_time_ms
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
@@ -668,8 +684,8 @@ impl BatchOperationService {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::common::stubs::{StubFileManagementUseCase, StubFileRetrievalUseCase};
|
||||
use std::sync::Arc;
|
||||
use crate::common::stubs::{StubFileRetrievalUseCase, StubFileManagementUseCase};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generic_batch_operation() {
|
||||
@@ -677,10 +693,10 @@ mod tests {
|
||||
let batch_service = BatchOperationService::new(
|
||||
Arc::new(StubFileRetrievalUseCase),
|
||||
Arc::new(StubFileManagementUseCase),
|
||||
Arc::new(FolderService::new(
|
||||
Arc::new(crate::common::stubs::StubFolderStoragePort)
|
||||
)),
|
||||
AppConfig::default()
|
||||
Arc::new(FolderService::new(Arc::new(
|
||||
crate::common::stubs::StubFolderStoragePort,
|
||||
))),
|
||||
AppConfig::default(),
|
||||
);
|
||||
|
||||
// Define a generic test operation
|
||||
@@ -700,7 +716,10 @@ mod tests {
|
||||
// Execute the batch operation
|
||||
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
|
||||
assert_eq!(result.stats.total, 5);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::application::dtos::calendar_dto::{
|
||||
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto,
|
||||
CreateEventDto, UpdateEventDto, CreateEventICalDto
|
||||
CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto,
|
||||
UpdateCalendarDto, UpdateEventDto,
|
||||
};
|
||||
use crate::application::ports::calendar_ports::{CalendarStoragePort, CalendarUseCase};
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
@@ -15,25 +15,32 @@ pub struct CalendarService {
|
||||
|
||||
impl CalendarService {
|
||||
pub fn new(calendar_storage: Arc<dyn CalendarStoragePort>) -> Self {
|
||||
Self {
|
||||
calendar_storage,
|
||||
}
|
||||
Self { calendar_storage }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
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
|
||||
// For now, we'll use a dummy implementation that needs to be completed
|
||||
|
||||
// In a real implementation, get user_id from current user context
|
||||
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:
|
||||
// 1. Get the current user ID from middleware
|
||||
// 2. Verify that the user has access to this calendar
|
||||
@@ -42,30 +49,38 @@ impl CalendarUseCase for CalendarService {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// 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 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"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> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// 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 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to delete this calendar"
|
||||
"You don't have permission to delete this calendar",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -79,13 +94,16 @@ impl CalendarUseCase for CalendarService {
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
|
||||
// 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 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to view this calendar"
|
||||
"You don't have permission to view this calendar",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -101,17 +119,30 @@ impl CalendarUseCase for CalendarService {
|
||||
async fn list_shared_calendars(&self) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
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 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(
|
||||
&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
|
||||
@@ -122,24 +153,35 @@ impl CalendarUseCase for CalendarService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"Only the calendar owner can change sharing settings"
|
||||
"Only the calendar owner can change sharing settings",
|
||||
));
|
||||
}
|
||||
|
||||
// Validate access_level
|
||||
match access_level {
|
||||
"read" | "write" | "owner" => {},
|
||||
_ => return Err(DomainError::new(
|
||||
"read" | "write" | "owner" => {}
|
||||
_ => {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Calendar",
|
||||
format!("Invalid access level: {}. Valid values are: read, write, owner", access_level)
|
||||
)),
|
||||
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(
|
||||
&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
|
||||
@@ -150,14 +192,19 @@ impl CalendarUseCase for CalendarService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"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(
|
||||
&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
|
||||
@@ -168,7 +215,7 @@ impl CalendarUseCase for CalendarService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"Only the calendar owner can view sharing settings"
|
||||
"Only the calendar owner can view sharing settings",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -179,50 +226,66 @@ impl CalendarUseCase for CalendarService {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// 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 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"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
|
||||
}
|
||||
|
||||
async fn create_event_from_ical(&self, event: CreateEventICalDto) -> Result<CalendarEventDto, DomainError> {
|
||||
async fn create_event_from_ical(
|
||||
&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
|
||||
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 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"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
|
||||
}
|
||||
|
||||
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError> {
|
||||
async fn update_event(
|
||||
&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
|
||||
let event = self.calendar_storage.get_event(event_id).await?;
|
||||
|
||||
// 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 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to update events in this calendar"
|
||||
"You don't have permission to update events in this calendar",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -236,13 +299,16 @@ impl CalendarUseCase for CalendarService {
|
||||
let event = self.calendar_storage.get_event(event_id).await?;
|
||||
|
||||
// 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 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to delete events in this calendar"
|
||||
"You don't have permission to delete events in this calendar",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -256,27 +322,41 @@ impl CalendarUseCase for CalendarService {
|
||||
let event = self.calendar_storage.get_event(event_id).await?;
|
||||
|
||||
// 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
|
||||
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 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"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)
|
||||
}
|
||||
|
||||
async fn list_events(&self, calendar_id: &str, limit: Option<i64>, offset: Option<i64>) -> Result<Vec<CalendarEventDto>, DomainError> {
|
||||
async fn list_events(
|
||||
&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
|
||||
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
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
@@ -285,7 +365,7 @@ impl CalendarUseCase for CalendarService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to view events in this calendar"
|
||||
"You don't have permission to view events in this calendar",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -294,9 +374,13 @@ impl CalendarUseCase for CalendarService {
|
||||
let limit = limit.unwrap_or(100);
|
||||
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 {
|
||||
self.calendar_storage.list_events_by_calendar(calendar_id).await
|
||||
self.calendar_storage
|
||||
.list_events_by_calendar(calendar_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,12 +388,15 @@ impl CalendarUseCase for CalendarService {
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
start: DateTime<Utc>,
|
||||
end: DateTime<Utc>
|
||||
end: DateTime<Utc>,
|
||||
) -> Result<Vec<CalendarEventDto>, DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// 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
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
@@ -318,85 +405,186 @@ impl CalendarUseCase for CalendarService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"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) ──
|
||||
|
||||
async fn create_calendar_for_user(&self, calendar: CreateCalendarDto, user_id: &str) -> Result<CalendarDto, DomainError> {
|
||||
self.calendar_storage.create_calendar(calendar, user_id).await
|
||||
async fn create_calendar_for_user(
|
||||
&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> {
|
||||
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?;
|
||||
async fn update_calendar_for_user(
|
||||
&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 {
|
||||
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> {
|
||||
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?;
|
||||
async fn delete_calendar_for_user(
|
||||
&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 {
|
||||
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
|
||||
}
|
||||
|
||||
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 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
async fn list_events_for_user(&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?;
|
||||
async fn list_events_for_user(
|
||||
&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?;
|
||||
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() {
|
||||
let limit = limit.unwrap_or(100);
|
||||
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 {
|
||||
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> {
|
||||
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_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> {
|
||||
let has_access = self
|
||||
.calendar_storage
|
||||
.check_calendar_access(calendar_id, user_id)
|
||||
.await?;
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
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> {
|
||||
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?;
|
||||
async fn create_event_from_ical_for_user(
|
||||
&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 {
|
||||
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
|
||||
}
|
||||
|
||||
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 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 {
|
||||
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
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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::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.
|
||||
///
|
||||
@@ -26,13 +26,20 @@ impl FavoritesUseCase for FavoritesService {
|
||||
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>> {
|
||||
info!("Getting favorites for user: {}", user_id);
|
||||
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)
|
||||
}
|
||||
|
||||
/// Add an item to user's favorites
|
||||
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" {
|
||||
return Err(DomainError::new(
|
||||
@@ -43,25 +50,48 @@ impl FavoritesUseCase for FavoritesService {
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Remove an item from user's favorites
|
||||
async fn remove_from_favorites(&self, 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?;
|
||||
async fn remove_from_favorites(
|
||||
&self,
|
||||
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!(
|
||||
"{} {} '{}' from favorites for user {}",
|
||||
if removed { "Successfully removed" } else { "Did not find" },
|
||||
item_type, item_id, user_id
|
||||
if removed {
|
||||
"Successfully removed"
|
||||
} else {
|
||||
"Did not find"
|
||||
},
|
||||
item_type,
|
||||
item_id,
|
||||
user_id
|
||||
);
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
|
||||
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::file_ports::FileManagementUseCase;
|
||||
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
|
||||
use crate::application::ports::trash_ports::TrashUseCase;
|
||||
use crate::common::errors::DomainError;
|
||||
use tracing::{debug, info, warn, error};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// Service for file management operations (move, delete).
|
||||
///
|
||||
@@ -76,9 +76,14 @@ impl FileManagementService {
|
||||
|
||||
/// Decrement dedup reference count; log result.
|
||||
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 {
|
||||
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]),
|
||||
Err(e) => warn!("⚠️ DEDUP: Failed to decrement reference: {}", e),
|
||||
}
|
||||
@@ -92,9 +97,16 @@ impl FileManagementUseCase for FileManagementService {
|
||||
file_id: &str,
|
||||
folder_id: Option<String>,
|
||||
) -> 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
|
||||
.file_repository
|
||||
.move_file(file_id, folder_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Error moving file (ID: {}): {}", file_id, e);
|
||||
e
|
||||
})?;
|
||||
@@ -109,14 +121,14 @@ impl FileManagementUseCase for FileManagementService {
|
||||
Ok(FileDto::from(moved_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> {
|
||||
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
|
||||
.file_repository
|
||||
.rename_file(file_id, new_name)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Error renaming file (ID: {}): {}", file_id, e);
|
||||
e
|
||||
})?;
|
||||
@@ -135,11 +147,7 @@ impl FileManagementUseCase for FileManagementService {
|
||||
}
|
||||
|
||||
/// Smart delete: trash-first with dedup reference cleanup.
|
||||
async fn delete_with_cleanup(
|
||||
&self,
|
||||
id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<bool, DomainError> {
|
||||
async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result<bool, DomainError> {
|
||||
// Step 1: Compute content hash for dedup tracking
|
||||
let content_hash = self.compute_content_hash(id).await;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use std::sync::Arc;
|
||||
|
||||
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::storage_ports::FileReadPort;
|
||||
use crate::application::ports::cache_ports::{WriteBehindCachePort, ContentCachePort};
|
||||
use crate::application::ports::transcode_ports::{ImageTranscodePort, OutputFormat};
|
||||
use crate::common::errors::DomainError;
|
||||
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> {
|
||||
@@ -150,10 +153,18 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
||||
|
||||
// ── Tier 0: Write-behind cache ───────────────────────
|
||||
if let Some(wb) = &self.write_behind
|
||||
&& let Some(pending) = wb.get_pending(id).await {
|
||||
debug!("⚡ TIER 0 Write-Behind HIT: {} ({} bytes)", file_name, pending.len());
|
||||
&& 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 {
|
||||
if let Some((t, m)) = self.try_transcode(id, &pending, &mime_type, file_size, true).await {
|
||||
if let Some((t, m)) = self
|
||||
.try_transcode(id, &pending, &mime_type, file_size, true)
|
||||
.await
|
||||
{
|
||||
(t, m)
|
||||
} else {
|
||||
(pending, mime_type.clone())
|
||||
@@ -161,32 +172,49 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
||||
} else {
|
||||
(pending, mime_type.clone())
|
||||
};
|
||||
return Ok((dto, OptimizedFileContent::Bytes {
|
||||
return Ok((
|
||||
dto,
|
||||
OptimizedFileContent::Bytes {
|
||||
data,
|
||||
mime_type: mime,
|
||||
was_transcoded: do_transcode,
|
||||
}));
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// ── Tier 1: Hot cache + transcode (<10 MB) ──────────
|
||||
if file_size < CACHE_THRESHOLD {
|
||||
// Check content cache first
|
||||
if let Some(cache) = &self.content_cache
|
||||
&& let Some((cached, _etag, _ct)) = cache.get(id).await {
|
||||
debug!("🔥 TIER 1 Cache HIT: {} ({} bytes)", file_name, cached.len());
|
||||
&& let Some((cached, _etag, _ct)) = cache.get(id).await
|
||||
{
|
||||
debug!(
|
||||
"🔥 TIER 1 Cache HIT: {} ({} bytes)",
|
||||
file_name,
|
||||
cached.len()
|
||||
);
|
||||
if do_transcode
|
||||
&& let Some((t, m)) = self.try_transcode(id, &cached, &mime_type, file_size, true).await {
|
||||
return Ok((dto, OptimizedFileContent::Bytes {
|
||||
&& let Some((t, m)) = self
|
||||
.try_transcode(id, &cached, &mime_type, file_size, true)
|
||||
.await
|
||||
{
|
||||
return Ok((
|
||||
dto,
|
||||
OptimizedFileContent::Bytes {
|
||||
data: t,
|
||||
mime_type: m,
|
||||
was_transcoded: true,
|
||||
}));
|
||||
},
|
||||
));
|
||||
}
|
||||
return Ok((dto, OptimizedFileContent::Bytes {
|
||||
return Ok((
|
||||
dto,
|
||||
OptimizedFileContent::Bytes {
|
||||
data: cached,
|
||||
mime_type: mime_type.clone(),
|
||||
was_transcoded: false,
|
||||
}));
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// Cache miss – load from disk
|
||||
@@ -197,27 +225,47 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
||||
// Store in cache
|
||||
if let Some(cache) = &self.content_cache {
|
||||
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
|
||||
&& let Some((t, m)) = self.try_transcode(id, &content_bytes, &mime_type, file_size, true).await {
|
||||
return Ok((dto, OptimizedFileContent::Bytes {
|
||||
&& let Some((t, m)) = self
|
||||
.try_transcode(id, &content_bytes, &mime_type, file_size, true)
|
||||
.await
|
||||
{
|
||||
return Ok((
|
||||
dto,
|
||||
OptimizedFileContent::Bytes {
|
||||
data: t,
|
||||
mime_type: m,
|
||||
was_transcoded: true,
|
||||
}));
|
||||
},
|
||||
));
|
||||
}
|
||||
return Ok((dto, OptimizedFileContent::Bytes {
|
||||
return Ok((
|
||||
dto,
|
||||
OptimizedFileContent::Bytes {
|
||||
data: content_bytes,
|
||||
mime_type: mime_type.clone(),
|
||||
was_transcoded: false,
|
||||
}));
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// ── Tier 2: MMAP (10–100 MB) ────────────────────────
|
||||
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 {
|
||||
Ok(mmap_content) => {
|
||||
return Ok((dto, OptimizedFileContent::Mmap(mmap_content)));
|
||||
@@ -230,17 +278,24 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
||||
}
|
||||
|
||||
// ── 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 {
|
||||
Ok(stream) => Ok((dto, OptimizedFileContent::Stream(Box::into_pin(stream)))),
|
||||
Err(e) => {
|
||||
warn!("Streaming failed, last-resort content load: {}", e);
|
||||
let content = self.file_read.get_file_content(id).await?;
|
||||
Ok((dto, OptimizedFileContent::Bytes {
|
||||
Ok((
|
||||
dto,
|
||||
OptimizedFileContent::Bytes {
|
||||
data: Bytes::from(content),
|
||||
mime_type: mime_type.clone(),
|
||||
was_transcoded: false,
|
||||
}))
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
use std::pin::Pin;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
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::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 tracing::{debug, info, warn};
|
||||
|
||||
@@ -47,7 +47,8 @@ pub struct FileUploadService {
|
||||
/// Optional dedup service for content-addressable storage
|
||||
dedup: Option<Arc<dyn DedupPort>>,
|
||||
/// 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 {
|
||||
@@ -92,7 +93,10 @@ impl FileUploadService {
|
||||
/// Run dedup tracking (non-fatal on failure).
|
||||
async fn run_dedup(&self, data: &[u8], content_type: &str) {
|
||||
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) => {
|
||||
if result.was_deduplicated() {
|
||||
info!(
|
||||
@@ -101,7 +105,10 @@ impl FileUploadService {
|
||||
result.size()
|
||||
);
|
||||
} else {
|
||||
info!("💾 DEDUP: new content stored (hash: {})", &result.hash()[..12]);
|
||||
info!(
|
||||
"💾 DEDUP: new content stored (hash: {})",
|
||||
&result.hash()[..12]
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -119,7 +126,10 @@ impl FileUploadService {
|
||||
let service_clone = Arc::clone(storage_service);
|
||||
tokio::spawn(async move {
|
||||
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),
|
||||
}
|
||||
});
|
||||
@@ -138,7 +148,10 @@ impl FileUploadUseCase for FileUploadService {
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> 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);
|
||||
self.maybe_update_storage_usage(&dto);
|
||||
Ok(dto)
|
||||
@@ -170,7 +183,8 @@ impl FileUploadUseCase for FileUploadService {
|
||||
// ─── TIER 1: Write-Behind (<256 KB) ──────────────────
|
||||
if total_size < WRITE_BEHIND_THRESHOLD
|
||||
&& 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()
|
||||
} else {
|
||||
@@ -188,13 +202,16 @@ impl FileUploadUseCase for FileUploadService {
|
||||
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
|
||||
)));
|
||||
return Err(DomainError::internal_error(
|
||||
"file",
|
||||
format!("Write-behind cache failed: {}", e),
|
||||
));
|
||||
}
|
||||
|
||||
info!("⚡ WRITE-BEHIND UPLOAD: {} (ID: {}, ~0ms latency)", name, dto.id);
|
||||
info!(
|
||||
"⚡ WRITE-BEHIND UPLOAD: {} (ID: {}, ~0ms latency)",
|
||||
name, dto.id
|
||||
);
|
||||
self.maybe_update_storage_usage(&dto);
|
||||
return Ok((dto, UploadStrategy::WriteBehind));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::application::ports::file_ports::{FileUploadUseCase, FileRetrievalUseCase, FileManagementUseCase, FileUseCaseFactory};
|
||||
use crate::application::services::file_upload_service::FileUploadService;
|
||||
use crate::application::services::file_retrieval_service::FileRetrievalService;
|
||||
use crate::application::services::file_management_service::FileManagementService;
|
||||
use crate::application::ports::file_ports::{
|
||||
FileManagementUseCase, FileRetrievalUseCase, FileUploadUseCase, FileUseCaseFactory,
|
||||
};
|
||||
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
|
||||
pub struct AppFileUseCaseFactory {
|
||||
@@ -16,7 +18,7 @@ impl AppFileUseCaseFactory {
|
||||
/// Creates a new factory for file use cases
|
||||
pub fn new(
|
||||
file_read_repository: Arc<dyn FileReadPort>,
|
||||
file_write_repository: Arc<dyn FileWritePort>
|
||||
file_write_repository: Arc<dyn FileWritePort>,
|
||||
) -> Self {
|
||||
Self {
|
||||
file_read_repository,
|
||||
@@ -35,6 +37,8 @@ impl FileUseCaseFactory for AppFileUseCaseFactory {
|
||||
}
|
||||
|
||||
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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::application::dtos::folder_dto::{CreateFolderDto, RenameFolderDto, MoveFolderDto, FolderDto};
|
||||
use crate::application::dtos::folder_dto::{
|
||||
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
|
||||
};
|
||||
use crate::application::ports::inbound::FolderUseCase;
|
||||
use crate::application::ports::outbound::FolderStoragePort;
|
||||
use crate::application::transactions::storage_transaction::StorageTransaction;
|
||||
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
|
||||
pub struct FolderService {
|
||||
@@ -36,28 +38,44 @@ impl FolderService {
|
||||
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![])
|
||||
}
|
||||
|
||||
async fn list_folders_paginated(
|
||||
&self,
|
||||
_parent_id: Option<&str>,
|
||||
_pagination: &crate::application::dtos::pagination::PaginationRequestDto
|
||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError> {
|
||||
Ok(crate::application::dtos::pagination::PaginatedResponseDto::new(
|
||||
_pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||
) -> Result<
|
||||
crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>,
|
||||
DomainError,
|
||||
> {
|
||||
Ok(
|
||||
crate::application::dtos::pagination::PaginatedResponseDto::new(
|
||||
vec![],
|
||||
0,
|
||||
10,
|
||||
0
|
||||
))
|
||||
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())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -79,7 +97,7 @@ impl FolderUseCase for FolderService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Folder",
|
||||
"Folder name cannot be empty"
|
||||
"Folder name cannot be empty",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -92,9 +110,16 @@ impl FolderUseCase for FolderService {
|
||||
}
|
||||
|
||||
// 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
|
||||
.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
|
||||
Ok(FolderDto::from(folder))
|
||||
@@ -102,9 +127,12 @@ impl FolderUseCase for FolderService {
|
||||
|
||||
/// Gets a folder by its ID
|
||||
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
|
||||
let folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {}: {}", id, e)))?;
|
||||
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!("Failed to get folder with ID: {}: {}", id, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
@@ -114,18 +142,32 @@ impl FolderUseCase for FolderService {
|
||||
// Convert the string path to StoragePath
|
||||
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
|
||||
.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))
|
||||
}
|
||||
|
||||
/// Lists folders within a parent folder
|
||||
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
|
||||
.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
|
||||
Ok(folders.into_iter().map(FolderDto::from).collect())
|
||||
@@ -135,20 +177,31 @@ impl FolderUseCase for FolderService {
|
||||
async fn list_folders_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto
|
||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError> {
|
||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>
|
||||
{
|
||||
// Validate and adjust pagination
|
||||
let pagination = pagination.validate_and_adjust();
|
||||
|
||||
// Get paginated folders and total count
|
||||
let (folders, total_items) = self.folder_storage.list_folders_paginated(
|
||||
let (folders, total_items) = self
|
||||
.folder_storage
|
||||
.list_folders_paginated(
|
||||
parent_id,
|
||||
pagination.offset(),
|
||||
pagination.limit(),
|
||||
true // Always include total for better UX
|
||||
true, // Always include total for better UX
|
||||
)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders with pagination in parent: {:?}: {}", parent_id, e)))?;
|
||||
.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
|
||||
let total = total_items.unwrap_or(folders.len());
|
||||
@@ -158,27 +211,34 @@ impl FolderUseCase for FolderService {
|
||||
folders.into_iter().map(FolderDto::from).collect(),
|
||||
pagination.page,
|
||||
pagination.page_size,
|
||||
total
|
||||
total,
|
||||
);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// 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
|
||||
if dto.name.is_empty() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Folder",
|
||||
"New folder name cannot be empty"
|
||||
"New folder name cannot be empty",
|
||||
));
|
||||
}
|
||||
|
||||
// Verify the folder exists
|
||||
let existing_folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for renaming: {}", id, e)))?;
|
||||
let existing_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!("Failed to get folder with ID: {} for renaming: {}", id, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Create transaction for renaming
|
||||
let mut transaction = StorageTransaction::new("rename_folder");
|
||||
@@ -201,13 +261,17 @@ impl FolderUseCase for FolderService {
|
||||
|
||||
async move {
|
||||
// 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_err(|e| DomainError::new(
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Folder",
|
||||
format!("Failed to rollback folder rename: {}", e)
|
||||
))
|
||||
format!("Failed to rollback folder rename: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
@@ -218,9 +282,12 @@ impl FolderUseCase for FolderService {
|
||||
transaction.commit().await?;
|
||||
|
||||
// Get the renamed folder
|
||||
let folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get renamed folder with ID: {}: {}", id, e)))?;
|
||||
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!("Failed to get renamed folder with ID: {}: {}", id, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
@@ -228,9 +295,12 @@ impl FolderUseCase for FolderService {
|
||||
/// Moves a folder to a new parent
|
||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError> {
|
||||
// Verify the source folder exists
|
||||
let source_folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for moving: {}", id, e)))?;
|
||||
let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!("Failed to get folder with ID: {} for moving: {}", id, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// If a parent_id is specified, verify it exists
|
||||
if let Some(parent_id) = &dto.parent_id {
|
||||
@@ -239,7 +309,7 @@ impl FolderUseCase for FolderService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Folder",
|
||||
"Cannot move a folder into itself"
|
||||
"Cannot move a folder into itself",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -276,13 +346,17 @@ impl FolderUseCase for FolderService {
|
||||
|
||||
async move {
|
||||
// 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_err(|e| DomainError::new(
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Folder",
|
||||
format!("Failed to rollback folder move: {}", e)
|
||||
))
|
||||
format!("Failed to rollback folder move: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
@@ -293,9 +367,12 @@ impl FolderUseCase for FolderService {
|
||||
transaction.commit().await?;
|
||||
|
||||
// Get the moved folder
|
||||
let folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get moved folder with ID: {}: {}", id, e)))?;
|
||||
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!("Failed to get moved folder with ID: {}: {}", id, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
@@ -303,15 +380,21 @@ impl FolderUseCase for FolderService {
|
||||
/// Deletes a folder
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> {
|
||||
// Verify the folder exists
|
||||
let _folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for deletion: {}", id, e)))?;
|
||||
let _folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!("Failed to get folder with ID: {} for deletion: {}", id, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// In a real implementation, we could verify permissions, dependencies, etc.
|
||||
|
||||
// Delete the folder
|
||||
self.folder_storage.delete_folder(id)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to delete folder with ID: {}: {}", id, e)))
|
||||
self.folder_storage.delete_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!("Failed to delete folder with ID: {}: {}", id, e),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
pub struct I18nApplicationService {
|
||||
@@ -31,7 +31,9 @@ impl I18nApplicationService {
|
||||
}
|
||||
}
|
||||
|
||||
Self { i18n_service: Arc::new(DummyI18nService) }
|
||||
Self {
|
||||
i18n_service: Arc::new(DummyI18nService),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new i18n application service
|
||||
|
||||
@@ -21,7 +21,7 @@ pub mod trash_service;
|
||||
mod trash_service_test;
|
||||
|
||||
// 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_retrieval_service::FileRetrievalService;
|
||||
pub use file_upload_service::FileUploadService;
|
||||
pub use file_use_case_factory::AppFileUseCaseFactory;
|
||||
|
||||
@@ -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::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.
|
||||
///
|
||||
@@ -27,17 +27,35 @@ impl RecentService {
|
||||
#[async_trait]
|
||||
impl RecentItemsUseCase for RecentService {
|
||||
/// 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);
|
||||
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?;
|
||||
info!("Retrieved {} recent items for user {}", items.len(), user_id);
|
||||
info!(
|
||||
"Retrieved {} recent items for user {}",
|
||||
items.len(),
|
||||
user_id
|
||||
);
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// Record access to an item
|
||||
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
|
||||
info!("Recording access to {} '{}' for user {}", item_type, item_id, user_id);
|
||||
async fn record_item_access(
|
||||
&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" {
|
||||
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.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(())
|
||||
}
|
||||
|
||||
/// Remove an item from recent
|
||||
async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
|
||||
info!("Removing {} '{}' from recent for user {}", item_type, item_id, user_id);
|
||||
async fn remove_from_recent(
|
||||
&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?;
|
||||
info!(
|
||||
"{} {} '{}' from recent items for user {}",
|
||||
if removed { "Successfully removed" } else { "Not found" },
|
||||
item_type, item_id, user_id
|
||||
if removed {
|
||||
"Successfully removed"
|
||||
} else {
|
||||
"Not found"
|
||||
},
|
||||
item_type,
|
||||
item_id,
|
||||
user_id
|
||||
);
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::sync::Mutex;
|
||||
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 crate::common::errors::Result;
|
||||
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
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::outbound::FolderStoragePort;
|
||||
use crate::application::ports::storage_ports::FileReadPort;
|
||||
use crate::common::errors::Result;
|
||||
|
||||
/**
|
||||
* Search service implementation for files and folders.
|
||||
@@ -155,7 +155,8 @@ impl SearchService {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -183,18 +184,21 @@ impl SearchService {
|
||||
if let Ok(mut cache) = self.search_cache.lock() {
|
||||
// If the cache is full, remove the oldest entry
|
||||
if cache.len() >= self.max_cache_size
|
||||
&& let Some((oldest_key, _)) = cache
|
||||
.iter()
|
||||
.min_by_key(|(_, result)| result.timestamp) {
|
||||
&& let Some((oldest_key, _)) =
|
||||
cache.iter().min_by_key(|(_, result)| result.timestamp)
|
||||
{
|
||||
let key_to_remove = oldest_key.clone();
|
||||
cache.remove(&key_to_remove);
|
||||
}
|
||||
|
||||
// Store the new result
|
||||
cache.insert(key, CachedSearchResult {
|
||||
cache.insert(
|
||||
key,
|
||||
CachedSearchResult {
|
||||
results,
|
||||
timestamp: Instant::now(),
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,18 +210,26 @@ impl SearchService {
|
||||
* @return Files that match the criteria
|
||||
*/
|
||||
fn filter_files(&self, files: Vec<FileDto>, criteria: &SearchCriteriaDto) -> Vec<FileDto> {
|
||||
files.into_iter()
|
||||
files
|
||||
.into_iter()
|
||||
.filter(|file| {
|
||||
// Filter by name
|
||||
if let Some(name_query) = &criteria.name_contains
|
||||
&& !file.name.to_lowercase().contains(&name_query.to_lowercase()) {
|
||||
&& !file
|
||||
.name
|
||||
.to_lowercase()
|
||||
.contains(&name_query.to_lowercase())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by file type (extension)
|
||||
if let Some(file_types) = &criteria.file_types {
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
@@ -228,34 +240,40 @@ impl SearchService {
|
||||
|
||||
// Filter by creation date
|
||||
if let Some(created_after) = criteria.created_after
|
||||
&& file.created_at < created_after {
|
||||
&& file.created_at < created_after
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(created_before) = criteria.created_before
|
||||
&& file.created_at > created_before {
|
||||
&& file.created_at > created_before
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by modification date
|
||||
if let Some(modified_after) = criteria.modified_after
|
||||
&& file.modified_at < modified_after {
|
||||
&& file.modified_at < modified_after
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(modified_before) = criteria.modified_before
|
||||
&& file.modified_at > modified_before {
|
||||
&& file.modified_at > modified_before
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by size
|
||||
if let Some(min_size) = criteria.min_size
|
||||
&& file.size < min_size {
|
||||
&& file.size < min_size
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(max_size) = criteria.max_size
|
||||
&& file.size > max_size {
|
||||
&& file.size > max_size
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -271,34 +289,47 @@ impl SearchService {
|
||||
* @param criteria Search criteria
|
||||
* @return Folders that match the criteria
|
||||
*/
|
||||
fn filter_folders(&self, folders: Vec<FolderDto>, criteria: &SearchCriteriaDto) -> Vec<FolderDto> {
|
||||
folders.into_iter()
|
||||
fn filter_folders(
|
||||
&self,
|
||||
folders: Vec<FolderDto>,
|
||||
criteria: &SearchCriteriaDto,
|
||||
) -> Vec<FolderDto> {
|
||||
folders
|
||||
.into_iter()
|
||||
.filter(|folder| {
|
||||
// Filter by name
|
||||
if let Some(name_query) = &criteria.name_contains
|
||||
&& !folder.name.to_lowercase().contains(&name_query.to_lowercase()) {
|
||||
&& !folder
|
||||
.name
|
||||
.to_lowercase()
|
||||
.contains(&name_query.to_lowercase())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by creation date
|
||||
if let Some(created_after) = criteria.created_after
|
||||
&& folder.created_at < created_after {
|
||||
&& folder.created_at < created_after
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(created_before) = criteria.created_before
|
||||
&& folder.created_at > created_before {
|
||||
&& folder.created_at > created_before
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by modification date
|
||||
if let Some(modified_after) = criteria.modified_after
|
||||
&& folder.modified_at < modified_after {
|
||||
&& folder.modified_at < modified_after
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(modified_before) = criteria.modified_before
|
||||
&& folder.modified_at > modified_before {
|
||||
&& folder.modified_at > modified_before
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -327,39 +358,35 @@ impl SearchService {
|
||||
let files = self.file_repository.list_files(current_folder_id).await?;
|
||||
|
||||
// Filter files according to criteria and add them to the results
|
||||
let filtered_files = self.filter_files(
|
||||
files.into_iter().map(FileDto::from).collect(),
|
||||
criteria
|
||||
);
|
||||
let filtered_files =
|
||||
self.filter_files(files.into_iter().map(FileDto::from).collect(), criteria);
|
||||
found_files.extend(filtered_files);
|
||||
|
||||
// If the search is recursive, process subfolders
|
||||
if criteria.recursive {
|
||||
// List subfolders
|
||||
let folders = self.folder_repository.list_folders(current_folder_id).await?;
|
||||
let folders = self
|
||||
.folder_repository
|
||||
.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(
|
||||
folders.into_iter().map(FolderDto::from).collect(),
|
||||
criteria
|
||||
);
|
||||
let filtered_folders: Vec<FolderDto> = self
|
||||
.filter_folders(folders.into_iter().map(FolderDto::from).collect(), criteria);
|
||||
|
||||
// Add filtered folders to the results
|
||||
found_folders.extend(filtered_folders.iter().cloned());
|
||||
|
||||
// Search recursively in each subfolder
|
||||
for folder in filtered_folders {
|
||||
self.search_recursive(
|
||||
Some(&folder.id),
|
||||
criteria,
|
||||
found_files,
|
||||
found_folders,
|
||||
).await?;
|
||||
self.search_recursive(Some(&folder.id), criteria, found_files, found_folders)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +418,8 @@ impl SearchUseCase for SearchService {
|
||||
&criteria,
|
||||
&mut found_files,
|
||||
&mut found_folders,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Apply pagination
|
||||
let total_count = found_files.len() + found_folders.len();
|
||||
|
||||
@@ -47,7 +47,9 @@ impl From<ShareServiceError> for DomainError {
|
||||
ShareServiceError::ItemNotFound(s) => DomainError::not_found("Item", s),
|
||||
ShareServiceError::AccessDenied(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::InvalidItemType(s) => DomainError::validation_error(s),
|
||||
ShareServiceError::Validation(s) => DomainError::validation_error(s),
|
||||
@@ -91,13 +93,23 @@ impl ShareService {
|
||||
self.file_repository
|
||||
.get_file(item_id) // Using the correct method from the FileStoragePort trait
|
||||
.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 => {
|
||||
self.folder_repository
|
||||
.get_folder(item_id) // Using the correct method from the FolderStoragePort trait
|
||||
.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(())
|
||||
@@ -105,8 +117,8 @@ impl ShareService {
|
||||
|
||||
/// Password hash using Argon2id (resistant to timing attacks and GPU attacks)
|
||||
fn hash_password(&self, password: &str) -> String {
|
||||
use argon2::{Argon2, PasswordHasher};
|
||||
use argon2::password_hash::SaltString;
|
||||
use argon2::{Argon2, PasswordHasher};
|
||||
use rand_core::OsRng;
|
||||
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
@@ -167,7 +179,9 @@ impl ShareUseCase for ShareService {
|
||||
.share_repository
|
||||
.find_share_by_id(id)
|
||||
.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
|
||||
if share.is_expired() {
|
||||
@@ -184,7 +198,9 @@ impl ShareUseCase for ShareService {
|
||||
.share_repository
|
||||
.find_share_by_token(token)
|
||||
.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
|
||||
if share.is_expired() {
|
||||
@@ -229,7 +245,9 @@ impl ShareUseCase for ShareService {
|
||||
.share_repository
|
||||
.find_share_by_id(id)
|
||||
.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
|
||||
if let Some(permissions_dto) = dto.permissions {
|
||||
@@ -264,7 +282,10 @@ impl ShareUseCase for ShareService {
|
||||
.map_err(|e| ShareServiceError::Repository(e.to_string()))?;
|
||||
|
||||
// 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> {
|
||||
@@ -300,12 +321,7 @@ impl ShareUseCase for ShareService {
|
||||
.collect();
|
||||
|
||||
// Create the paginated result
|
||||
let paginated = PaginatedResponseDto::new(
|
||||
share_dtos,
|
||||
page,
|
||||
per_page,
|
||||
total
|
||||
);
|
||||
let paginated = PaginatedResponseDto::new(share_dtos, page, per_page, total);
|
||||
|
||||
Ok(paginated)
|
||||
}
|
||||
@@ -320,7 +336,9 @@ impl ShareUseCase for ShareService {
|
||||
.share_repository
|
||||
.find_share_by_token(token)
|
||||
.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
|
||||
if share.is_expired() {
|
||||
@@ -329,9 +347,7 @@ impl ShareUseCase for ShareService {
|
||||
|
||||
// Verify the password using the infrastructure port
|
||||
match share.password_hash() {
|
||||
Some(hash) => {
|
||||
self.password_hasher.verify_password(password, hash)
|
||||
}
|
||||
Some(hash) => self.password_hasher.verify_password(password, hash),
|
||||
None => Ok(true), // No password required
|
||||
}
|
||||
}
|
||||
@@ -342,7 +358,9 @@ impl ShareUseCase for ShareService {
|
||||
.share_repository
|
||||
.find_share_by_token(token)
|
||||
.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
|
||||
if share.is_expired() {
|
||||
@@ -365,9 +383,9 @@ impl ShareUseCase for ShareService {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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::ports::auth_ports::PasswordHasherPort;
|
||||
use crate::application::ports::share_ports::ShareStoragePort;
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::domain::repositories::folder_repository::FolderRepository;
|
||||
use async_trait::async_trait;
|
||||
@@ -391,12 +409,17 @@ mod tests {
|
||||
|
||||
#[async_trait]
|
||||
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" {
|
||||
let file = crate::domain::entities::file::File::new(
|
||||
id.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,
|
||||
"text/plain".to_string(),
|
||||
None,
|
||||
@@ -408,7 +431,10 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
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!()
|
||||
}
|
||||
|
||||
@@ -419,7 +445,10 @@ mod tests {
|
||||
async fn get_file_stream(
|
||||
&self,
|
||||
_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!()
|
||||
}
|
||||
|
||||
@@ -428,7 +457,10 @@ mod tests {
|
||||
_id: &str,
|
||||
_start: 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!()
|
||||
}
|
||||
|
||||
@@ -436,7 +468,10 @@ mod tests {
|
||||
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!()
|
||||
}
|
||||
|
||||
@@ -447,16 +482,25 @@ mod tests {
|
||||
|
||||
#[async_trait]
|
||||
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!()
|
||||
}
|
||||
|
||||
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" {
|
||||
let folder = crate::domain::entities::folder::Folder::new(
|
||||
id.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,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -466,23 +510,44 @@ 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(
|
||||
&self,
|
||||
_storage_path: &crate::domain::services::path_service::StoragePath,
|
||||
) -> Result<crate::domain::entities::folder::Folder, DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn list_folders(&self, _parent_id: Option<&str>) -> Result<Vec<crate::domain::entities::folder::Folder>, DomainError> {
|
||||
async fn list_folders(
|
||||
&self,
|
||||
_parent_id: Option<&str>,
|
||||
) -> Result<Vec<crate::domain::entities::folder::Folder>, DomainError> {
|
||||
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_paginated(
|
||||
&self,
|
||||
_parent_id: Option<&str>,
|
||||
_offset: usize,
|
||||
_limit: usize,
|
||||
_include_total: bool,
|
||||
) -> Result<(Vec<crate::domain::entities::folder::Folder>, Option<usize>), DomainError>
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn rename_folder(&self, _id: &str, _new_name: String) -> 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!()
|
||||
}
|
||||
|
||||
async fn move_folder(&self, _id: &str, _new_parent_id: Option<&str>) -> Result<crate::domain::entities::folder::Folder, DomainError> {
|
||||
async fn move_folder(
|
||||
&self,
|
||||
_id: &str,
|
||||
_new_parent_id: Option<&str>,
|
||||
) -> Result<crate::domain::entities::folder::Folder, DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -490,11 +555,17 @@ mod tests {
|
||||
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!()
|
||||
}
|
||||
|
||||
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!()
|
||||
}
|
||||
|
||||
@@ -502,7 +573,11 @@ mod tests {
|
||||
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!()
|
||||
}
|
||||
|
||||
@@ -540,7 +615,8 @@ mod tests {
|
||||
async fn find_share_by_id(&self, id: &str) -> Result<Share, DomainError> {
|
||||
let shares = self.shares.lock().unwrap();
|
||||
|
||||
shares.get(id)
|
||||
shares
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| DomainError::not_found("Share", id))
|
||||
}
|
||||
@@ -549,19 +625,26 @@ mod tests {
|
||||
let tokens = self.tokens.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))?;
|
||||
|
||||
shares.get(id)
|
||||
shares
|
||||
.get(id)
|
||||
.cloned()
|
||||
.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 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)
|
||||
.cloned()
|
||||
.collect();
|
||||
@@ -587,7 +670,8 @@ mod tests {
|
||||
let mut tokens = self.tokens.lock().unwrap();
|
||||
|
||||
// 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))?;
|
||||
|
||||
// Remove token mapping
|
||||
@@ -599,10 +683,16 @@ mod tests {
|
||||
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 user_shares: Vec<Share> = shares.values()
|
||||
let user_shares: Vec<Share> = shares
|
||||
.values()
|
||||
.filter(|s| s.created_by() == user_id)
|
||||
.cloned()
|
||||
.collect();
|
||||
@@ -610,10 +700,7 @@ mod tests {
|
||||
let total = user_shares.len();
|
||||
|
||||
// Apply pagination
|
||||
let paginated = user_shares.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect();
|
||||
let paginated = user_shares.into_iter().skip(offset).take(limit).collect();
|
||||
|
||||
Ok((paginated, total))
|
||||
}
|
||||
@@ -628,7 +715,8 @@ mod tests {
|
||||
let folder_repo = Arc::new(MockFolderRepository);
|
||||
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
|
||||
let dto = CreateShareDto {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use async_trait::async_trait;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::application::ports::outbound::{FolderStoragePort, IdMappingPort, StoragePort};
|
||||
use crate::domain::entities::folder::Folder;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::application::ports::outbound::{IdMappingPort, StoragePort, FolderStoragePort};
|
||||
|
||||
/// Storage mediator specific errors
|
||||
#[derive(Debug, Error)]
|
||||
@@ -48,13 +48,19 @@ pub trait StorageMediator: Send + Sync + 'static {
|
||||
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
|
||||
|
||||
/// 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
|
||||
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool>;
|
||||
|
||||
/// 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)
|
||||
fn resolve_path(&self, relative_path: &Path) -> PathBuf;
|
||||
@@ -66,7 +72,10 @@ pub trait StorageMediator: Send + Sync + 'static {
|
||||
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()>;
|
||||
|
||||
/// 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
|
||||
@@ -77,8 +86,16 @@ pub struct FileSystemStorageMediator {
|
||||
}
|
||||
|
||||
impl FileSystemStorageMediator {
|
||||
pub fn new(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 }
|
||||
pub fn new(
|
||||
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
|
||||
@@ -110,21 +127,29 @@ impl StorageMediator for StubStorageMediator {
|
||||
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
|
||||
Ok(StoragePath::root())
|
||||
}
|
||||
|
||||
async fn get_folder(&self, _folder_id: &str) -> StorageMediatorResult<Folder> {
|
||||
// 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> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -132,7 +157,10 @@ impl StorageMediator for StubStorageMediator {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -148,7 +176,10 @@ impl StorageMediator for StubStorageMediator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_storage_directory(&self, _storage_path: &StoragePath) -> StorageMediatorResult<()> {
|
||||
async fn ensure_storage_directory(
|
||||
&self,
|
||||
_storage_path: &StoragePath,
|
||||
) -> StorageMediatorResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -156,11 +187,17 @@ impl StorageMediator for StubStorageMediator {
|
||||
#[async_trait]
|
||||
impl StorageMediator for FileSystemStorageMediator {
|
||||
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)?;
|
||||
|
||||
// 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)?;
|
||||
|
||||
// Convert StoragePath to PathBuf
|
||||
@@ -169,18 +206,27 @@ impl StorageMediator for FileSystemStorageMediator {
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
// 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)?;
|
||||
|
||||
Ok(storage_path)
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
Ok(folder)
|
||||
@@ -195,7 +241,10 @@ impl StorageMediator for FileSystemStorageMediator {
|
||||
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);
|
||||
|
||||
// Check if it exists as a file (not as a directory)
|
||||
@@ -213,7 +262,10 @@ impl StorageMediator for FileSystemStorageMediator {
|
||||
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);
|
||||
|
||||
// Check if it exists as a directory
|
||||
@@ -238,28 +290,35 @@ impl StorageMediator for FileSystemStorageMediator {
|
||||
|
||||
// Create directories if they don't exist
|
||||
if !abs_path.exists() {
|
||||
tokio::fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?;
|
||||
tokio::fs::create_dir_all(&abs_path).await.map_err(|e| {
|
||||
StorageMediatorError::AccessError(format!("Could not create directory: {}", e))
|
||||
})?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||
));
|
||||
return Err(StorageMediatorError::InvalidPath(format!(
|
||||
"Path exists but is not a directory: {}",
|
||||
abs_path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Create directories if they don't exist
|
||||
if !abs_path.exists() {
|
||||
tokio::fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("Could not create directory: {}", e)))?;
|
||||
tokio::fs::create_dir_all(&abs_path).await.map_err(|e| {
|
||||
StorageMediatorError::AccessError(format!("Could not create directory: {}", e))
|
||||
})?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||
));
|
||||
return Err(StorageMediatorError::InvalidPath(format!(
|
||||
"Path exists but is not a directory: {}",
|
||||
abs_path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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::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.
|
||||
@@ -41,9 +41,14 @@ impl StorageUsageService {
|
||||
let total_usage = self.calculate_user_storage_usage(username).await?;
|
||||
|
||||
// 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)
|
||||
.await?;
|
||||
|
||||
info!("Updated storage usage for user {} to {} bytes", user_id, total_usage);
|
||||
info!(
|
||||
"Updated storage usage for user {} to {} bytes",
|
||||
user_id, total_usage
|
||||
);
|
||||
|
||||
Ok(total_usage)
|
||||
}
|
||||
@@ -54,7 +59,10 @@ impl StorageUsageService {
|
||||
|
||||
// First, try to find the user's home 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()))?;
|
||||
|
||||
// Find the user's home folder (named "My Folder - {username}")
|
||||
@@ -68,7 +76,11 @@ impl StorageUsageService {
|
||||
for folder in &all_folders {
|
||||
if folder.name() == home_folder_name {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -95,7 +107,9 @@ impl StorageUsageService {
|
||||
let mut total_size: i64 = 0;
|
||||
|
||||
// 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()))?;
|
||||
|
||||
// Sum the size of all files
|
||||
@@ -107,14 +121,18 @@ impl StorageUsageService {
|
||||
let repo_clone = repo.clone(); // Clone the repository
|
||||
|
||||
// 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 {
|
||||
Ok(size) => {
|
||||
total_size += size;
|
||||
},
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -160,9 +178,12 @@ impl StorageUsagePort for StorageUsageService {
|
||||
let task = task::spawn(async move {
|
||||
match service_clone.update_user_storage_usage(&user_id).await {
|
||||
Ok(usage) => {
|
||||
debug!("Updated storage usage for user {}: {} bytes", user_id, usage);
|
||||
debug!(
|
||||
"Updated storage usage for user {}: {} bytes",
|
||||
user_id, usage
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to update storage for user {}: {}", user_id, e);
|
||||
Err(e)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info, instrument};
|
||||
use uuid::Uuid;
|
||||
|
||||
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::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::repositories::trash_repository::TrashRepository;
|
||||
|
||||
@@ -86,12 +86,18 @@ impl TrashService {
|
||||
let user_uuid = Uuid::parse_str(user_id)
|
||||
.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) => {
|
||||
if item.user_id() != user_uuid {
|
||||
error!(
|
||||
"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(
|
||||
"TrashItem",
|
||||
@@ -123,16 +129,17 @@ impl TrashUseCase for TrashService {
|
||||
|
||||
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
|
||||
|
||||
let dtos = items.into_iter()
|
||||
.map(|item| self.to_dto(item))
|
||||
.collect();
|
||||
let dtos = items.into_iter().map(|item| self.to_dto(item)).collect();
|
||||
|
||||
Ok(dtos)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
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);
|
||||
|
||||
// Note: We do NOT call validate_user_ownership here because the item
|
||||
@@ -145,10 +152,13 @@ impl TrashUseCase for TrashService {
|
||||
Ok(uuid) => {
|
||||
debug!("Valid item UUID: {}", uuid);
|
||||
uuid
|
||||
},
|
||||
}
|
||||
Err(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
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,10 +167,13 @@ impl TrashUseCase for TrashService {
|
||||
Ok(uuid) => {
|
||||
debug!("Valid user UUID: {}", uuid);
|
||||
uuid
|
||||
},
|
||||
}
|
||||
Err(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
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -174,13 +187,13 @@ impl TrashUseCase for TrashService {
|
||||
Ok(file) => {
|
||||
debug!("File found: {} ({})", file.name(), item_id);
|
||||
file
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error getting file: {} - {}", item_id, e);
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::NotFound,
|
||||
"File",
|
||||
format!("Error retrieving file {}: {}", item_id, e)
|
||||
format!("Error retrieving file {}: {}", item_id, e),
|
||||
));
|
||||
}
|
||||
};
|
||||
@@ -198,17 +211,24 @@ impl TrashUseCase for TrashService {
|
||||
original_path,
|
||||
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
|
||||
info!("Adding file {} to trash index", item_id);
|
||||
match self.trash_repository.add_to_trash(&trashed_item).await {
|
||||
Ok(_) => {
|
||||
debug!("File added to trash index successfully");
|
||||
},
|
||||
}
|
||||
Err(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),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -217,28 +237,33 @@ impl TrashUseCase for TrashService {
|
||||
match self.file_write_port.move_to_trash(item_id).await {
|
||||
Ok(_) => {
|
||||
debug!("File physically moved to trash successfully: {}", item_id);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error physically moving file to trash: {} - {}", item_id, e);
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"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);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
"folder" => {
|
||||
// Get the folder to verify it exists and capture its data
|
||||
let folder = self.folder_storage_port.get_folder(item_id).await
|
||||
.map_err(|e| DomainError::new(
|
||||
let folder = self
|
||||
.folder_storage_port
|
||||
.get_folder(item_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::NotFound,
|
||||
"Folder",
|
||||
format!("Error retrieving folder {}: {}", item_id, e)
|
||||
))?;
|
||||
format!("Error retrieving folder {}: {}", item_id, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let original_path = folder.storage_path().to_string();
|
||||
|
||||
@@ -258,22 +283,32 @@ impl TrashUseCase for TrashService {
|
||||
Ok(_) => debug!("Successfully added folder to trash repository"),
|
||||
Err(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
|
||||
self.folder_storage_port.move_to_trash(item_id).await
|
||||
.map_err(|e| DomainError::new(
|
||||
self.folder_storage_port
|
||||
.move_to_trash(item_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Folder",
|
||||
format!("Error moving folder {} to trash: {}", item_id, e)
|
||||
))?;
|
||||
format!("Error moving folder {} to trash: {}", item_id, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
debug!("Folder moved to trash: {}", item_id);
|
||||
Ok(())
|
||||
},
|
||||
_ => Err(DomainError::validation_error(format!("Invalid item type: {}", item_type))),
|
||||
}
|
||||
_ => Err(DomainError::validation_error(format!(
|
||||
"Invalid item type: {}",
|
||||
item_type
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,10 +320,13 @@ impl TrashUseCase for TrashService {
|
||||
Ok(id) => {
|
||||
info!("Trash UUID parsed successfully: {}", id);
|
||||
id
|
||||
},
|
||||
}
|
||||
Err(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
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -296,21 +334,31 @@ impl TrashUseCase for TrashService {
|
||||
Ok(id) => {
|
||||
info!("User UUID parsed successfully: {}", id);
|
||||
id
|
||||
},
|
||||
}
|
||||
Err(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
|
||||
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 {
|
||||
Ok(Some(item)) => {
|
||||
info!("Found item in trash: ID={}, Type={:?}, OriginalID={}",
|
||||
trash_id, item.item_type(), item.original_id());
|
||||
info!(
|
||||
"Found item in trash: ID={}, Type={:?}, OriginalID={}",
|
||||
trash_id,
|
||||
item.item_type(),
|
||||
item.original_id()
|
||||
);
|
||||
|
||||
// Restore based on type
|
||||
match item.item_type() {
|
||||
@@ -319,15 +367,25 @@ impl TrashUseCase for TrashService {
|
||||
let file_id = item.original_id().to_string();
|
||||
let original_path = item.original_path().to_string();
|
||||
|
||||
info!("Restoring file from trash: ID={}, OriginalPath={}", file_id, original_path);
|
||||
match self.file_write_port.restore_from_trash(&file_id, &original_path).await {
|
||||
info!(
|
||||
"Restoring file from trash: ID={}, OriginalPath={}",
|
||||
file_id, original_path
|
||||
);
|
||||
match self
|
||||
.file_write_port
|
||||
.restore_from_trash(&file_id, &original_path)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("Successfully restored file from trash: {}", file_id);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// Check if the error is because the file is 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
|
||||
} else {
|
||||
// Return error for other kinds of errors
|
||||
@@ -335,34 +393,53 @@ impl TrashUseCase for TrashService {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"File",
|
||||
format!("Error restoring file {} from trash: {}", file_id, e)
|
||||
format!(
|
||||
"Error restoring file {} from trash: {}",
|
||||
file_id, e
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
TrashedItemType::Folder => {
|
||||
// Restore the folder to its original location
|
||||
let folder_id = item.original_id().to_string();
|
||||
let original_path = item.original_path().to_string();
|
||||
|
||||
info!("Restoring folder from trash: ID={}, OriginalPath={}", folder_id, original_path);
|
||||
match self.folder_storage_port.restore_from_trash(&folder_id, &original_path).await {
|
||||
info!(
|
||||
"Restoring folder from trash: ID={}, OriginalPath={}",
|
||||
folder_id, original_path
|
||||
);
|
||||
match self
|
||||
.folder_storage_port
|
||||
.restore_from_trash(&folder_id, &original_path)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("Successfully restored folder from trash: {}", folder_id);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// Check if the error is because the folder is 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
|
||||
} else {
|
||||
// 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(
|
||||
ErrorKind::InternalError,
|
||||
"Folder",
|
||||
format!("Error restoring folder {} from trash: {}", folder_id, e)
|
||||
format!(
|
||||
"Error restoring folder {} from trash: {}",
|
||||
folder_id, e
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -371,32 +448,48 @@ impl TrashUseCase for TrashService {
|
||||
}
|
||||
|
||||
// Always remove the item from the trash index to maintain consistency
|
||||
info!("Removing item from trash index after restoration: {}", trash_id);
|
||||
match self.trash_repository.restore_from_trash(&trash_uuid, &user_uuid).await {
|
||||
info!(
|
||||
"Removing item from trash index after restoration: {}",
|
||||
trash_id
|
||||
);
|
||||
match self
|
||||
.trash_repository
|
||||
.restore_from_trash(&trash_uuid, &user_uuid)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("Successfully removed entry from trash index: {}", trash_id);
|
||||
},
|
||||
}
|
||||
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(
|
||||
ErrorKind::InternalError,
|
||||
"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);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
Ok(None) => {
|
||||
// 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(())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -404,16 +497,22 @@ impl TrashUseCase for TrashService {
|
||||
|
||||
#[instrument(skip(self))]
|
||||
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) {
|
||||
Ok(id) => {
|
||||
info!("Trash UUID parsed successfully: {}", id);
|
||||
id
|
||||
},
|
||||
}
|
||||
Err(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
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -421,21 +520,31 @@ impl TrashUseCase for TrashService {
|
||||
Ok(id) => {
|
||||
info!("User UUID parsed successfully: {}", id);
|
||||
id
|
||||
},
|
||||
}
|
||||
Err(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
|
||||
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 {
|
||||
Ok(Some(item)) => {
|
||||
info!("Found item in trash: ID={}, Type={:?}, OriginalID={}",
|
||||
trash_id, item.item_type(), item.original_id());
|
||||
info!(
|
||||
"Found item in trash: ID={}, Type={:?}, OriginalID={}",
|
||||
trash_id,
|
||||
item.item_type(),
|
||||
item.original_id()
|
||||
);
|
||||
|
||||
// Permanently delete based on type
|
||||
match item.item_type() {
|
||||
@@ -447,44 +556,63 @@ impl TrashUseCase for TrashService {
|
||||
match self.file_write_port.delete_file_permanently(&file_id).await {
|
||||
Ok(_) => {
|
||||
info!("Successfully deleted file permanently: {}", file_id);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// Check if the file is not found - in that case, we can continue
|
||||
// because we still want to remove the item from the trash index
|
||||
if format!("{}", e).contains("not found") {
|
||||
info!("File not found, may already have been deleted: {}", file_id);
|
||||
info!(
|
||||
"File not found, may already have been deleted: {}",
|
||||
file_id
|
||||
);
|
||||
} else {
|
||||
// Return error for other types of errors
|
||||
error!("Error permanently deleting file: {} - {}", file_id, e);
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"File",
|
||||
format!("Error deleting file {} permanently: {}", file_id, e)
|
||||
format!(
|
||||
"Error deleting file {} permanently: {}",
|
||||
file_id, e
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
TrashedItemType::Folder => {
|
||||
// Permanently delete the folder
|
||||
let folder_id = item.original_id().to_string();
|
||||
|
||||
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(_) => {
|
||||
info!("Successfully deleted folder permanently: {}", folder_id);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// Check if the folder is not found - in that case, we can continue
|
||||
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 {
|
||||
// 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(
|
||||
ErrorKind::InternalError,
|
||||
"Folder",
|
||||
format!("Error deleting folder {} permanently: {}", folder_id, e)
|
||||
format!(
|
||||
"Error deleting folder {} permanently: {}",
|
||||
folder_id, e
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -494,31 +622,44 @@ impl TrashUseCase for TrashService {
|
||||
|
||||
// Always remove the item from trash index to maintain consistency
|
||||
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(_) => {
|
||||
info!("Successfully removed entry from trash index: {}", trash_id);
|
||||
},
|
||||
}
|
||||
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(
|
||||
ErrorKind::InternalError,
|
||||
"Trash",
|
||||
format!("Error removing trash entry: {}", e)
|
||||
format!("Error removing trash entry: {}", e),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
info!("Item permanently deleted from trash: {}", trash_id);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
Ok(None) => {
|
||||
// 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(())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -543,11 +684,15 @@ impl TrashUseCase for TrashService {
|
||||
if let Err(e) = self.file_write_port.delete_file_permanently(&file_id).await {
|
||||
error!("Error permanently deleting file {}: {}", file_id, e);
|
||||
}
|
||||
},
|
||||
}
|
||||
TrashedItemType::Folder => {
|
||||
// Permanently delete the folder
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 uuid::Uuid;
|
||||
use bytes::Bytes;
|
||||
use chrono::Utc;
|
||||
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::folder::Folder;
|
||||
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::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
|
||||
struct MockTrashRepository {
|
||||
@@ -40,7 +40,8 @@ impl TrashRepository for MockTrashRepository {
|
||||
|
||||
async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> {
|
||||
let items = self.trash_items.lock().unwrap();
|
||||
let user_items = items.values()
|
||||
let user_items = items
|
||||
.values()
|
||||
.filter(|item| item.user_id() == *user_id)
|
||||
.cloned()
|
||||
.collect();
|
||||
@@ -49,7 +50,8 @@ impl TrashRepository for MockTrashRepository {
|
||||
|
||||
async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result<Option<TrashedItem>> {
|
||||
let items = self.trash_items.lock().unwrap();
|
||||
let item = items.get(id)
|
||||
let item = items
|
||||
.get(id)
|
||||
.filter(|item| item.user_id() == *user_id)
|
||||
.cloned();
|
||||
Ok(item)
|
||||
@@ -58,7 +60,8 @@ impl TrashRepository for MockTrashRepository {
|
||||
async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
|
||||
let mut items = self.trash_items.lock().unwrap();
|
||||
if let Some(item) = items.get(id)
|
||||
&& item.user_id() == *user_id {
|
||||
&& item.user_id() == *user_id
|
||||
{
|
||||
items.remove(id);
|
||||
}
|
||||
Ok(())
|
||||
@@ -67,7 +70,8 @@ impl TrashRepository for MockTrashRepository {
|
||||
async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
|
||||
let mut items = self.trash_items.lock().unwrap();
|
||||
if let Some(item) = items.get(id)
|
||||
&& item.user_id() == *user_id {
|
||||
&& item.user_id() == *user_id
|
||||
{
|
||||
items.remove(id);
|
||||
}
|
||||
Ok(())
|
||||
@@ -82,7 +86,8 @@ impl TrashRepository for MockTrashRepository {
|
||||
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> {
|
||||
let items = self.trash_items.lock().unwrap();
|
||||
let now = Utc::now();
|
||||
let expired = items.values()
|
||||
let expired = items
|
||||
.values()
|
||||
.filter(|item| item.deletion_date() <= now)
|
||||
.cloned()
|
||||
.collect();
|
||||
@@ -111,7 +116,8 @@ impl MockFileRepository {
|
||||
100,
|
||||
"text/plain".to_string(),
|
||||
None,
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut files = self.files.lock().unwrap();
|
||||
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![])
|
||||
}
|
||||
|
||||
@@ -140,7 +149,10 @@ impl FileReadPort for MockFileRepository {
|
||||
async fn get_file_stream(
|
||||
&self,
|
||||
_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!()
|
||||
}
|
||||
|
||||
@@ -149,7 +161,10 @@ impl FileReadPort for MockFileRepository {
|
||||
_id: &str,
|
||||
_start: 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!()
|
||||
}
|
||||
|
||||
@@ -183,7 +198,9 @@ impl FileWritePort for MockFileRepository {
|
||||
_name: String,
|
||||
_folder_id: Option<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> {
|
||||
unimplemented!()
|
||||
}
|
||||
@@ -208,7 +225,11 @@ impl FileWritePort for MockFileRepository {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -234,7 +255,11 @@ 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 trashed = self.trashed_files.lock().unwrap();
|
||||
|
||||
@@ -242,7 +267,10 @@ impl FileWritePort for MockFileRepository {
|
||||
files.insert(id.to_string(), file);
|
||||
Ok(())
|
||||
} 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() {
|
||||
Ok(())
|
||||
} 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,7 +306,8 @@ impl MockFolderRepository {
|
||||
name.to_string(),
|
||||
StoragePath::from_string(path),
|
||||
None,
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut folders = self.folders.lock().unwrap();
|
||||
folders.insert(id.to_string(), folder);
|
||||
@@ -284,7 +316,11 @@ impl MockFolderRepository {
|
||||
|
||||
#[async_trait]
|
||||
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!()
|
||||
}
|
||||
|
||||
@@ -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!()
|
||||
}
|
||||
|
||||
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![])
|
||||
}
|
||||
|
||||
@@ -315,11 +357,19 @@ impl FolderRepository for MockFolderRepository {
|
||||
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!()
|
||||
}
|
||||
|
||||
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!()
|
||||
}
|
||||
|
||||
@@ -327,7 +377,10 @@ impl FolderRepository for MockFolderRepository {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -347,7 +400,11 @@ 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 trashed = self.trashed_folders.lock().unwrap();
|
||||
|
||||
@@ -355,7 +412,10 @@ impl FolderRepository for MockFolderRepository {
|
||||
folders.insert(id.to_string(), folder);
|
||||
Ok(())
|
||||
} 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() {
|
||||
Ok(())
|
||||
} 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),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,20 +468,42 @@ mod tests {
|
||||
let user_uuid = Uuid::parse_str(user_id).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];
|
||||
|
||||
assert_eq!(trash_item.original_id().to_string(), 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.original_id().to_string(),
|
||||
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");
|
||||
|
||||
// Verify file is moved in file repository
|
||||
let files = file_repo.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!(trashed_files.get(file_id).is_some(), "File should be in trash storage");
|
||||
assert!(
|
||||
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]
|
||||
@@ -446,18 +531,38 @@ mod tests {
|
||||
let result = service.move_to_trash(folder_id, "folder", user_id).await;
|
||||
|
||||
// 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
|
||||
let user_uuid = Uuid::parse_str(user_id).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];
|
||||
|
||||
assert_eq!(trash_item.original_id().to_string(), 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.original_id().to_string(),
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -482,7 +587,10 @@ mod tests {
|
||||
|
||||
// Add a test file and move it to trash
|
||||
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
|
||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||
@@ -493,18 +601,32 @@ mod tests {
|
||||
let result = service.restore_item(&trash_id, user_id).await;
|
||||
|
||||
// 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
|
||||
let files = file_repo.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!(trashed_files.get(file_id).is_none(), "File should no longer be in trash storage");
|
||||
assert!(
|
||||
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
|
||||
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]
|
||||
@@ -527,7 +649,10 @@ mod tests {
|
||||
|
||||
// Add a test file and move it to trash
|
||||
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
|
||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||
@@ -538,18 +663,32 @@ mod tests {
|
||||
let result = service.delete_permanently(&trash_id, user_id).await;
|
||||
|
||||
// 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
|
||||
let files = file_repo.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!(trashed_files.get(file_id).is_none(), "File should not be in trash storage");
|
||||
assert!(
|
||||
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
|
||||
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]
|
||||
@@ -582,13 +721,27 @@ mod tests {
|
||||
|
||||
// Add test files and folders
|
||||
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));
|
||||
service.move_to_trash(file_id, "file", user_id).await.unwrap();
|
||||
file_repo.add_test_file(
|
||||
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() {
|
||||
folder_repo.add_test_folder(folder_id, &format!("folder{}", i), &format!("/test/path/folder{}", i));
|
||||
service.move_to_trash(folder_id, "folder", user_id).await.unwrap();
|
||||
folder_repo.add_test_folder(
|
||||
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
|
||||
@@ -606,15 +759,27 @@ mod tests {
|
||||
for file_id in &file_ids {
|
||||
let files = file_repo.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!(trashed_files.get(*file_id).is_none(), "File should not be in trash storage");
|
||||
assert!(
|
||||
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 {
|
||||
let folders = folder_repo.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!(trashed_folders.get(*folder_id).is_none(), "Folder should not be in trash storage");
|
||||
assert!(
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Type for async operations and rollbacks
|
||||
type TransactionOp = Pin<Box<dyn Future<Output = Result<(), DomainError>> + Send>>;
|
||||
@@ -64,10 +64,19 @@ impl StorageTransaction {
|
||||
match op().await {
|
||||
Ok(()) => {
|
||||
completed_ops.push(i);
|
||||
tracing::trace!("Operation {} completed in transaction: {}", i, transaction_name);
|
||||
tracing::trace!(
|
||||
"Operation {} completed in transaction: {}",
|
||||
i,
|
||||
transaction_name
|
||||
);
|
||||
}
|
||||
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
|
||||
self.rollback(completed_ops).await?;
|
||||
@@ -75,8 +84,9 @@ impl StorageTransaction {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Transaction",
|
||||
format!("Transaction '{}' failed: {}", transaction_name, e)
|
||||
).with_source(e));
|
||||
format!("Transaction '{}' failed: {}", transaction_name, e),
|
||||
)
|
||||
.with_source(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,8 +113,12 @@ impl StorageTransaction {
|
||||
// Swap with an empty function
|
||||
let rollback = std::mem::replace(rb, Box::new(|| Box::pin(async { Ok(()) })));
|
||||
if let Err(e) = rollback().await {
|
||||
tracing::error!("Error in rollback of operation {} in transaction {}: {}",
|
||||
i, self.name, e);
|
||||
tracing::error!(
|
||||
"Error in rollback of operation {} in transaction {}: {}",
|
||||
i,
|
||||
self.name,
|
||||
e
|
||||
);
|
||||
rollback_errors.push(e);
|
||||
}
|
||||
}
|
||||
@@ -113,14 +127,20 @@ impl StorageTransaction {
|
||||
|
||||
// If there were errors during rollback, report them
|
||||
if !rollback_errors.is_empty() {
|
||||
tracing::error!("Errors during transaction rollback {}: {} errors",
|
||||
self.name, rollback_errors.len());
|
||||
tracing::error!(
|
||||
"Errors during transaction rollback {}: {} errors",
|
||||
self.name,
|
||||
rollback_errors.len()
|
||||
);
|
||||
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Transaction",
|
||||
format!("Errors during transaction '{}' rollback: {} errors",
|
||||
self.name, rollback_errors.len())
|
||||
format!(
|
||||
"Errors during transaction '{}' rollback: {} errors",
|
||||
self.name,
|
||||
rollback_errors.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
+68
-40
@@ -1,6 +1,6 @@
|
||||
use std::time::Duration;
|
||||
use std::path::PathBuf;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Cache configuration
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -316,20 +316,36 @@ impl OidcConfig {
|
||||
if let Ok(v) = env::var("OXICLOUD_OIDC_ENABLED") {
|
||||
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_CLIENT_ID") { cfg.client_id = 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_ISSUER_URL") {
|
||||
cfg.issuer_url = v;
|
||||
}
|
||||
if let Ok(v) = env::var("OXICLOUD_OIDC_CLIENT_ID") {
|
||||
cfg.client_id = 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") {
|
||||
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") {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -421,7 +437,8 @@ impl AppConfig {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -434,15 +451,17 @@ impl AppConfig {
|
||||
config.database.connection_string = connection_string;
|
||||
}
|
||||
|
||||
if let Ok(max_connections) = env::var("OXICLOUD_DB_MAX_CONNECTIONS")
|
||||
.map(|v| v.parse::<u32>())
|
||||
&& let Ok(val) = max_connections {
|
||||
if let Ok(max_connections) =
|
||||
env::var("OXICLOUD_DB_MAX_CONNECTIONS").map(|v| v.parse::<u32>())
|
||||
&& let Ok(val) = max_connections
|
||||
{
|
||||
config.database.max_connections = val;
|
||||
}
|
||||
|
||||
if let Ok(min_connections) = env::var("OXICLOUD_DB_MIN_CONNECTIONS")
|
||||
.map(|v| v.parse::<u32>())
|
||||
&& let Ok(val) = min_connections {
|
||||
if let Ok(min_connections) =
|
||||
env::var("OXICLOUD_DB_MIN_CONNECTIONS").map(|v| v.parse::<u32>())
|
||||
&& let Ok(val) = min_connections
|
||||
{
|
||||
config.database.min_connections = val;
|
||||
}
|
||||
|
||||
@@ -468,46 +487,50 @@ impl AppConfig {
|
||||
tracing::warn!("==========================================================");
|
||||
}
|
||||
|
||||
if let Ok(access_token_expiry) = env::var("OXICLOUD_ACCESS_TOKEN_EXPIRY_SECS")
|
||||
.map(|v| v.parse::<i64>())
|
||||
&& let Ok(val) = access_token_expiry {
|
||||
if let Ok(access_token_expiry) =
|
||||
env::var("OXICLOUD_ACCESS_TOKEN_EXPIRY_SECS").map(|v| v.parse::<i64>())
|
||||
&& let Ok(val) = access_token_expiry
|
||||
{
|
||||
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>())
|
||||
&& let Ok(val) = refresh_token_expiry {
|
||||
if let Ok(refresh_token_expiry) =
|
||||
env::var("OXICLOUD_REFRESH_TOKEN_EXPIRY_SECS").map(|v| v.parse::<i64>())
|
||||
&& let Ok(val) = refresh_token_expiry
|
||||
{
|
||||
config.auth.refresh_token_expiry_secs = val;
|
||||
}
|
||||
|
||||
// Feature flags
|
||||
if let Ok(enable_auth) = env::var("OXICLOUD_ENABLE_AUTH")
|
||||
.map(|v| v.parse::<bool>())
|
||||
&& let Ok(val) = enable_auth {
|
||||
if let Ok(enable_auth) = env::var("OXICLOUD_ENABLE_AUTH").map(|v| v.parse::<bool>())
|
||||
&& let Ok(val) = enable_auth
|
||||
{
|
||||
config.features.enable_auth = val;
|
||||
}
|
||||
|
||||
if let Ok(enable_user_storage_quotas) = env::var("OXICLOUD_ENABLE_USER_STORAGE_QUOTAS")
|
||||
.map(|v| v.parse::<bool>())
|
||||
&& let Ok(val) = enable_user_storage_quotas {
|
||||
if let Ok(enable_user_storage_quotas) =
|
||||
env::var("OXICLOUD_ENABLE_USER_STORAGE_QUOTAS").map(|v| v.parse::<bool>())
|
||||
&& let Ok(val) = enable_user_storage_quotas
|
||||
{
|
||||
config.features.enable_user_storage_quotas = val;
|
||||
}
|
||||
|
||||
if let Ok(enable_file_sharing) = env::var("OXICLOUD_ENABLE_FILE_SHARING")
|
||||
.map(|v| v.parse::<bool>())
|
||||
&& let Ok(val) = enable_file_sharing {
|
||||
if let Ok(enable_file_sharing) =
|
||||
env::var("OXICLOUD_ENABLE_FILE_SHARING").map(|v| v.parse::<bool>())
|
||||
&& 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>())
|
||||
&& 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>())
|
||||
&& let Ok(val) = enable_search
|
||||
{
|
||||
config.features.enable_search = val;
|
||||
}
|
||||
|
||||
@@ -548,8 +571,13 @@ impl AppConfig {
|
||||
|
||||
// Validate OIDC config when enabled
|
||||
if config.oidc.enabled
|
||||
&& (config.oidc.issuer_url.is_empty() || config.oidc.client_id.is_empty() || 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.issuer_url.is_empty()
|
||||
|| config.oidc.client_id.is_empty()
|
||||
|| 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;
|
||||
}
|
||||
|
||||
|
||||
+237
-159
@@ -1,56 +1,61 @@
|
||||
use sqlx::PgPool;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::application::services::auth_application_service::AuthApplicationService;
|
||||
use crate::application::services::admin_settings_service::AdminSettingsService;
|
||||
use crate::application::services::auth_application_service::AuthApplicationService;
|
||||
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository;
|
||||
use crate::infrastructure::repositories::trash_fs_repository::TrashFsRepository;
|
||||
use crate::infrastructure::repositories::share_fs_repository::ShareFsRepository;
|
||||
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||
use crate::infrastructure::services::file_system_i18n_service::FileSystemI18nService;
|
||||
use crate::infrastructure::services::id_mapping_service::IdMappingService;
|
||||
use crate::infrastructure::services::id_mapping_optimizer::IdMappingOptimizer;
|
||||
use crate::infrastructure::services::file_metadata_cache::FileMetadataCache;
|
||||
use crate::infrastructure::services::file_content_cache::{FileContentCache, FileContentCacheConfig};
|
||||
use crate::infrastructure::services::buffer_pool::BufferPool;
|
||||
use crate::infrastructure::services::trash_cleanup_service::TrashCleanupService;
|
||||
use crate::application::services::folder_service::FolderService;
|
||||
use crate::application::services::i18n_application_service::I18nApplicationService;
|
||||
use crate::application::services::trash_service::TrashService;
|
||||
use crate::application::services::search_service::SearchService;
|
||||
use crate::application::services::share_service::ShareService;
|
||||
use crate::application::services::favorites_service::FavoritesService;
|
||||
use crate::application::services::recent_service::RecentService;
|
||||
use crate::application::ports::trash_ports::TrashUseCase;
|
||||
use crate::application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator};
|
||||
use crate::application::ports::inbound::{FolderUseCase, SearchUseCase};
|
||||
use crate::application::ports::outbound::FolderStoragePort;
|
||||
use crate::application::ports::favorites_ports::FavoritesUseCase;
|
||||
use crate::application::ports::recent_ports::RecentItemsUseCase;
|
||||
use crate::application::ports::file_ports::{FileUploadUseCase, FileRetrievalUseCase, FileManagementUseCase, FileUseCaseFactory};
|
||||
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
|
||||
use crate::infrastructure::repositories::{FileFsReadRepository, FileFsWriteRepository};
|
||||
use crate::application::services::{FileUploadService, FileRetrievalService, FileManagementService, AppFileUseCaseFactory};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::services::i18n_service::I18nService;
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::application::ports::cache_ports::{WriteBehindCachePort, ContentCachePort};
|
||||
use crate::application::ports::thumbnail_ports::ThumbnailPort;
|
||||
use crate::application::ports::transcode_ports::ImageTranscodePort;
|
||||
use crate::application::ports::dedup_ports::DedupPort;
|
||||
use crate::application::ports::cache_ports::{ContentCachePort, WriteBehindCachePort};
|
||||
use crate::application::ports::chunked_upload_ports::ChunkedUploadPort;
|
||||
use crate::application::ports::compression_ports::CompressionPort;
|
||||
use crate::application::ports::dedup_ports::DedupPort;
|
||||
use crate::application::ports::favorites_ports::FavoritesUseCase;
|
||||
use crate::application::ports::file_ports::{
|
||||
FileManagementUseCase, FileRetrievalUseCase, FileUploadUseCase, FileUseCaseFactory,
|
||||
};
|
||||
use crate::application::ports::inbound::{FolderUseCase, SearchUseCase};
|
||||
use crate::application::ports::outbound::FolderStoragePort;
|
||||
use crate::application::ports::recent_ports::RecentItemsUseCase;
|
||||
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
|
||||
use crate::application::ports::thumbnail_ports::ThumbnailPort;
|
||||
use crate::application::ports::transcode_ports::ImageTranscodePort;
|
||||
use crate::application::ports::trash_ports::TrashUseCase;
|
||||
use crate::application::ports::zip_ports::ZipPort;
|
||||
use crate::application::services::favorites_service::FavoritesService;
|
||||
use crate::application::services::folder_service::FolderService;
|
||||
use crate::application::services::i18n_application_service::I18nApplicationService;
|
||||
use crate::application::services::recent_service::RecentService;
|
||||
use crate::application::services::search_service::SearchService;
|
||||
use crate::application::services::share_service::ShareService;
|
||||
use crate::application::services::storage_mediator::{FileSystemStorageMediator, StorageMediator};
|
||||
use crate::application::services::trash_service::TrashService;
|
||||
use crate::application::services::{
|
||||
AppFileUseCaseFactory, FileManagementService, FileRetrievalService, FileUploadService,
|
||||
};
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::services::i18n_service::I18nService;
|
||||
use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository;
|
||||
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||
use crate::infrastructure::repositories::share_fs_repository::ShareFsRepository;
|
||||
use crate::infrastructure::repositories::trash_fs_repository::TrashFsRepository;
|
||||
use crate::infrastructure::repositories::{FileFsReadRepository, FileFsWriteRepository};
|
||||
use crate::infrastructure::services::buffer_pool::BufferPool;
|
||||
use crate::infrastructure::services::file_content_cache::{
|
||||
FileContentCache, FileContentCacheConfig,
|
||||
};
|
||||
use crate::infrastructure::services::file_metadata_cache::FileMetadataCache;
|
||||
use crate::infrastructure::services::file_system_i18n_service::FileSystemI18nService;
|
||||
use crate::infrastructure::services::id_mapping_optimizer::IdMappingOptimizer;
|
||||
use crate::infrastructure::services::id_mapping_service::IdMappingService;
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
use crate::infrastructure::services::trash_cleanup_service::TrashCleanupService;
|
||||
|
||||
use crate::common::stubs::{
|
||||
StubZipPort, StubCompressionPort, StubIdMappingService, StubStorageMediator,
|
||||
StubFileReadPort, StubFileWritePort, StubFolderStoragePort,
|
||||
StubI18nService, StubFolderUseCase, StubFileUploadUseCase,
|
||||
StubFileRetrievalUseCase, StubFileManagementUseCase, StubFileUseCaseFactory,
|
||||
StubSearchUseCase,
|
||||
StubCompressionPort, StubFileManagementUseCase, StubFileReadPort, StubFileRetrievalUseCase,
|
||||
StubFileUploadUseCase, StubFileUseCaseFactory, StubFileWritePort, StubFolderStoragePort,
|
||||
StubFolderUseCase, StubI18nService, StubIdMappingService, StubSearchUseCase,
|
||||
StubStorageMediator, StubZipPort,
|
||||
};
|
||||
|
||||
/// Factory for the different application components
|
||||
@@ -107,20 +112,16 @@ impl AppServiceFactory {
|
||||
|
||||
// ID mapping service for folders
|
||||
let folder_id_mapping_path = self.storage_path.join("folder_ids.json");
|
||||
let folder_id_mapping_service = Arc::new(
|
||||
IdMappingService::new(folder_id_mapping_path).await?
|
||||
);
|
||||
let folder_id_mapping_service =
|
||||
Arc::new(IdMappingService::new(folder_id_mapping_path).await?);
|
||||
|
||||
// ID mapping service for files
|
||||
let file_id_mapping_path = self.storage_path.join("file_ids.json");
|
||||
let file_id_mapping_service = Arc::new(
|
||||
IdMappingService::new(file_id_mapping_path).await?
|
||||
);
|
||||
let file_id_mapping_service = Arc::new(IdMappingService::new(file_id_mapping_path).await?);
|
||||
|
||||
// Optimizer with batch processing and caching
|
||||
let id_mapping_optimizer = Arc::new(
|
||||
IdMappingOptimizer::new(folder_id_mapping_service.clone())
|
||||
);
|
||||
let id_mapping_optimizer =
|
||||
Arc::new(IdMappingOptimizer::new(folder_id_mapping_service.clone()));
|
||||
|
||||
// Start optimizer cleanup task
|
||||
IdMappingOptimizer::start_cleanup_task(id_mapping_optimizer.clone());
|
||||
@@ -131,18 +132,21 @@ impl AppServiceFactory {
|
||||
&self.storage_path,
|
||||
5000, // max 5000 thumbnails in cache
|
||||
100 * 1024 * 1024, // max 100MB cache
|
||||
)
|
||||
),
|
||||
);
|
||||
// Initialize thumbnail directories
|
||||
thumbnail_service.initialize().await?;
|
||||
|
||||
// Write-behind cache for instant uploads of small files
|
||||
let write_behind_cache = crate::infrastructure::services::write_behind_cache::WriteBehindCache::new();
|
||||
let write_behind_cache =
|
||||
crate::infrastructure::services::write_behind_cache::WriteBehindCache::new();
|
||||
|
||||
// Chunked upload service for large files (>10MB)
|
||||
let chunked_temp_dir = std::path::PathBuf::from(&self.storage_path).join(".uploads");
|
||||
let chunked_upload_service = Arc::new(
|
||||
crate::infrastructure::services::chunked_upload_service::ChunkedUploadService::new(chunked_temp_dir)
|
||||
crate::infrastructure::services::chunked_upload_service::ChunkedUploadService::new(
|
||||
chunked_temp_dir,
|
||||
),
|
||||
);
|
||||
|
||||
// Image transcoding service for automatic WebP conversion
|
||||
@@ -151,22 +155,24 @@ impl AppServiceFactory {
|
||||
&self.storage_path,
|
||||
2000, // max 2000 transcoded images in cache
|
||||
50 * 1024 * 1024, // max 50MB in-memory cache
|
||||
)
|
||||
),
|
||||
);
|
||||
image_transcode_service.initialize().await?;
|
||||
|
||||
// Deduplication service for removing duplicate files
|
||||
let dedup_service = Arc::new(
|
||||
crate::infrastructure::services::dedup_service::DedupService::new(&self.storage_path)
|
||||
crate::infrastructure::services::dedup_service::DedupService::new(&self.storage_path),
|
||||
);
|
||||
dedup_service.initialize().await?;
|
||||
|
||||
// Compression service (gzip)
|
||||
let compression_service: Arc<dyn CompressionPort> = Arc::new(
|
||||
crate::infrastructure::services::compression_service::GzipCompressionService::new()
|
||||
crate::infrastructure::services::compression_service::GzipCompressionService::new(),
|
||||
);
|
||||
|
||||
tracing::info!("Core services initialized: path service, cache manager, file content cache, ID mapping, thumbnails, write-behind cache, chunked upload, image transcode, dedup, compression");
|
||||
tracing::info!(
|
||||
"Core services initialized: path service, cache manager, file content cache, ID mapping, thumbnails, write-behind cache, chunked upload, image transcode, dedup, compression"
|
||||
);
|
||||
|
||||
// NOTE: zip_service requires ApplicationServices (FileRetrievalUseCase, FolderUseCase) which are
|
||||
// created later. It will be set via AppState::with_zip_service() after application services are ready.
|
||||
@@ -192,9 +198,8 @@ impl AppServiceFactory {
|
||||
/// Initializes the repository services
|
||||
pub fn create_repository_services(&self, core: &CoreServices) -> RepositoryServices {
|
||||
// Storage mediator - uses stub initially, will be replaced after folder repo is ready
|
||||
let storage_mediator_stub: Arc<dyn StorageMediator> = Arc::new(
|
||||
FileSystemStorageMediator::new_stub()
|
||||
);
|
||||
let storage_mediator_stub: Arc<dyn StorageMediator> =
|
||||
Arc::new(FileSystemStorageMediator::new_stub());
|
||||
|
||||
// Folder repository — implements FolderStoragePort directly
|
||||
let folder_repository = Arc::new(FolderFsRepository::new(
|
||||
@@ -208,13 +213,11 @@ impl AppServiceFactory {
|
||||
let storage_mediator: Arc<dyn StorageMediator> = Arc::new(FileSystemStorageMediator::new(
|
||||
folder_repository.clone() as Arc<dyn FolderStoragePort>,
|
||||
core.path_service.clone(),
|
||||
core.id_mapping_optimizer.clone()
|
||||
core.id_mapping_optimizer.clone(),
|
||||
));
|
||||
|
||||
// Metadata cache
|
||||
let metadata_cache = Arc::new(
|
||||
FileMetadataCache::default_with_config(core.config.clone())
|
||||
);
|
||||
let metadata_cache = Arc::new(FileMetadataCache::default_with_config(core.config.clone()));
|
||||
|
||||
// Start metadata cache cleanup task
|
||||
let cache_clone = metadata_cache.clone();
|
||||
@@ -229,7 +232,7 @@ impl AppServiceFactory {
|
||||
// Parallel file processor
|
||||
let parallel_processor = Arc::new(ParallelFileProcessor::new_with_buffer_pool(
|
||||
core.config.clone(),
|
||||
buffer_pool.clone()
|
||||
buffer_pool.clone(),
|
||||
));
|
||||
|
||||
// Separate file repositories for reading and writing
|
||||
@@ -254,16 +257,17 @@ impl AppServiceFactory {
|
||||
));
|
||||
|
||||
// I18n repository
|
||||
let i18n_repository = Arc::new(FileSystemI18nService::new(
|
||||
self.locales_path.clone()
|
||||
));
|
||||
let i18n_repository = Arc::new(FileSystemI18nService::new(self.locales_path.clone()));
|
||||
|
||||
// Trash repository
|
||||
let trash_repository = if core.config.features.enable_trash {
|
||||
Some(Arc::new(TrashFsRepository::new(
|
||||
self.storage_path.as_path(),
|
||||
core.id_mapping_service.clone(),
|
||||
)) as Arc<dyn crate::domain::repositories::trash_repository::TrashRepository>)
|
||||
))
|
||||
as Arc<
|
||||
dyn crate::domain::repositories::trash_repository::TrashRepository,
|
||||
>)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -289,9 +293,7 @@ impl AppServiceFactory {
|
||||
trash_service: Option<Arc<dyn TrashUseCase>>,
|
||||
) -> ApplicationServices {
|
||||
// Main services
|
||||
let folder_service = Arc::new(FolderService::new(
|
||||
repos.folder_repository.clone()
|
||||
));
|
||||
let folder_service = Arc::new(FolderService::new(repos.folder_repository.clone()));
|
||||
|
||||
// Refactored services with all infrastructure ports
|
||||
let file_upload_service = Arc::new(FileUploadService::new_full(
|
||||
@@ -318,12 +320,10 @@ impl AppServiceFactory {
|
||||
|
||||
let file_use_case_factory = Arc::new(AppFileUseCaseFactory::new(
|
||||
repos.file_read_repository.clone(),
|
||||
repos.file_write_repository.clone()
|
||||
repos.file_write_repository.clone(),
|
||||
));
|
||||
|
||||
let i18n_service = Arc::new(I18nApplicationService::new(
|
||||
repos.i18n_repository.clone()
|
||||
));
|
||||
let i18n_service = Arc::new(I18nApplicationService::new(repos.i18n_repository.clone()));
|
||||
|
||||
// Search service with cache
|
||||
let search_service: Option<Arc<dyn SearchUseCase>> = Some(Arc::new(SearchService::new(
|
||||
@@ -397,9 +397,7 @@ impl AppServiceFactory {
|
||||
return None;
|
||||
}
|
||||
|
||||
let share_repository = Arc::new(ShareFsRepository::new(
|
||||
Arc::new(self.config.clone())
|
||||
));
|
||||
let share_repository = Arc::new(ShareFsRepository::new(Arc::new(self.config.clone())));
|
||||
|
||||
// Build a password hasher for share password verification
|
||||
let password_hasher: Arc<dyn crate::application::ports::auth_ports::PasswordHasherPort> =
|
||||
@@ -418,12 +416,9 @@ impl AppServiceFactory {
|
||||
}
|
||||
|
||||
/// Creates the favorites service (requires database)
|
||||
pub fn create_favorites_service(
|
||||
&self,
|
||||
db_pool: &Arc<PgPool>,
|
||||
) -> Arc<dyn FavoritesUseCase> {
|
||||
pub fn create_favorites_service(&self, db_pool: &Arc<PgPool>) -> Arc<dyn FavoritesUseCase> {
|
||||
let repo = Arc::new(
|
||||
crate::infrastructure::repositories::pg::FavoritesPgRepository::new(db_pool.clone())
|
||||
crate::infrastructure::repositories::pg::FavoritesPgRepository::new(db_pool.clone()),
|
||||
);
|
||||
let service = Arc::new(FavoritesService::new(repo));
|
||||
tracing::info!("Favorites service initialized");
|
||||
@@ -431,16 +426,12 @@ impl AppServiceFactory {
|
||||
}
|
||||
|
||||
/// Creates the recent items service (requires database)
|
||||
pub fn create_recent_service(
|
||||
&self,
|
||||
db_pool: &Arc<PgPool>,
|
||||
) -> Arc<dyn RecentItemsUseCase> {
|
||||
pub fn create_recent_service(&self, db_pool: &Arc<PgPool>) -> Arc<dyn RecentItemsUseCase> {
|
||||
let repo = Arc::new(
|
||||
crate::infrastructure::repositories::pg::RecentItemsPgRepository::new(db_pool.clone())
|
||||
crate::infrastructure::repositories::pg::RecentItemsPgRepository::new(db_pool.clone()),
|
||||
);
|
||||
let service = Arc::new(RecentService::new(
|
||||
repo,
|
||||
50 // Maximum recent items per user
|
||||
repo, 50, // Maximum recent items per user
|
||||
));
|
||||
tracing::info!("Recent items service initialized");
|
||||
service
|
||||
@@ -471,7 +462,10 @@ impl AppServiceFactory {
|
||||
/// Preloads directories into cache
|
||||
pub async fn preload_cache(&self, metadata_cache: &FileMetadataCache) {
|
||||
tracing::info!("Preloading common directories to warm up cache...");
|
||||
if let Ok(count) = metadata_cache.preload_directory(&self.storage_path, true, 1).await {
|
||||
if let Ok(count) = metadata_cache
|
||||
.preload_directory(&self.storage_path, true, 1)
|
||||
.await
|
||||
{
|
||||
tracing::info!("Preloaded {} directory entries into cache", count);
|
||||
}
|
||||
}
|
||||
@@ -483,13 +477,13 @@ impl AppServiceFactory {
|
||||
db_pool: &Arc<PgPool>,
|
||||
) -> Arc<dyn crate::application::ports::storage_ports::StorageUsagePort> {
|
||||
let user_repository = Arc::new(
|
||||
crate::infrastructure::repositories::pg::UserPgRepository::new(db_pool.clone())
|
||||
crate::infrastructure::repositories::pg::UserPgRepository::new(db_pool.clone()),
|
||||
);
|
||||
let service = Arc::new(
|
||||
crate::application::services::storage_usage_service::StorageUsageService::new(
|
||||
repos.file_read_repository.clone(),
|
||||
user_repository,
|
||||
)
|
||||
),
|
||||
);
|
||||
tracing::info!("Storage usage service initialized");
|
||||
service
|
||||
@@ -521,7 +515,9 @@ impl AppServiceFactory {
|
||||
// 6. Database-dependent services
|
||||
let mut favorites_service: Option<Arc<dyn FavoritesUseCase>> = None;
|
||||
let mut recent_service: Option<Arc<dyn RecentItemsUseCase>> = None;
|
||||
let mut storage_usage_service: Option<Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>> = None;
|
||||
let mut storage_usage_service: Option<
|
||||
Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>,
|
||||
> = None;
|
||||
let mut auth_services: Option<crate::common::di::AuthServices> = None;
|
||||
|
||||
if let Some(ref pool) = db_pool {
|
||||
@@ -541,7 +537,9 @@ impl AppServiceFactory {
|
||||
&self.config,
|
||||
pool.clone(),
|
||||
Some(apps.folder_service_concrete.clone()),
|
||||
).await {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(services) => {
|
||||
tracing::info!("Authentication services initialized successfully");
|
||||
auth_services = Some(services);
|
||||
@@ -564,7 +562,7 @@ impl AppServiceFactory {
|
||||
crate::infrastructure::services::zip_service::ZipService::new(
|
||||
apps.file_retrieval_service.clone(),
|
||||
apps.folder_service.clone(),
|
||||
)
|
||||
),
|
||||
);
|
||||
let mut core = core;
|
||||
core.zip_service = zip_service;
|
||||
@@ -592,7 +590,7 @@ impl AppServiceFactory {
|
||||
// 10b. Wire admin settings service when auth + DB are available
|
||||
if let (Some(auth_svc), Some(pool)) = (&app_state.auth_service, &db_pool) {
|
||||
let settings_repo = Arc::new(
|
||||
crate::infrastructure::repositories::pg::SettingsPgRepository::new(pool.clone())
|
||||
crate::infrastructure::repositories::pg::SettingsPgRepository::new(pool.clone()),
|
||||
);
|
||||
let server_base_url = self.config.base_url();
|
||||
|
||||
@@ -608,20 +606,30 @@ impl AppServiceFactory {
|
||||
|
||||
// Hot-reload OIDC from DB settings if configured
|
||||
match admin_svc.load_effective_oidc_config().await {
|
||||
Ok(eff) if eff.enabled && !eff.issuer_url.is_empty()
|
||||
&& !eff.client_id.is_empty() && !eff.client_secret.is_empty() =>
|
||||
Ok(eff)
|
||||
if eff.enabled
|
||||
&& !eff.issuer_url.is_empty()
|
||||
&& !eff.client_id.is_empty()
|
||||
&& !eff.client_secret.is_empty() =>
|
||||
{
|
||||
let oidc_svc = Arc::new(
|
||||
crate::infrastructure::services::oidc_service::OidcService::new(eff.clone())
|
||||
crate::infrastructure::services::oidc_service::OidcService::new(
|
||||
eff.clone(),
|
||||
),
|
||||
);
|
||||
auth_svc.auth_application_service.reload_oidc(oidc_svc, eff);
|
||||
tracing::info!("OIDC config loaded from admin settings (database)");
|
||||
}
|
||||
Ok(_) => {
|
||||
tracing::info!("No active OIDC config in admin settings — using env vars or defaults");
|
||||
tracing::info!(
|
||||
"No active OIDC config in admin settings — using env vars or defaults"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to load OIDC settings from database (table may not exist yet): {}", e);
|
||||
tracing::warn!(
|
||||
"Failed to load OIDC settings from database (table may not exist yet): {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,11 +639,17 @@ impl AppServiceFactory {
|
||||
// 11. Wire CalDAV/CardDAV services when database is available
|
||||
if let Some(ref pool) = db_pool {
|
||||
// CalDAV
|
||||
let calendar_repo: Arc<dyn crate::domain::repositories::calendar_repository::CalendarRepository> = Arc::new(
|
||||
crate::infrastructure::repositories::pg::CalendarPgRepository::new(pool.clone())
|
||||
let calendar_repo: Arc<
|
||||
dyn crate::domain::repositories::calendar_repository::CalendarRepository,
|
||||
> = Arc::new(
|
||||
crate::infrastructure::repositories::pg::CalendarPgRepository::new(pool.clone()),
|
||||
);
|
||||
let event_repo: Arc<dyn crate::domain::repositories::calendar_event_repository::CalendarEventRepository> = Arc::new(
|
||||
crate::infrastructure::repositories::pg::CalendarEventPgRepository::new(pool.clone())
|
||||
let event_repo: Arc<
|
||||
dyn crate::domain::repositories::calendar_event_repository::CalendarEventRepository,
|
||||
> = Arc::new(
|
||||
crate::infrastructure::repositories::pg::CalendarEventPgRepository::new(
|
||||
pool.clone(),
|
||||
),
|
||||
);
|
||||
let calendar_storage = Arc::new(
|
||||
crate::infrastructure::adapters::calendar_storage_adapter::CalendarStorageAdapter::new(
|
||||
@@ -644,19 +658,32 @@ impl AppServiceFactory {
|
||||
)
|
||||
);
|
||||
let calendar_service = Arc::new(
|
||||
crate::application::services::calendar_service::CalendarService::new(calendar_storage)
|
||||
crate::application::services::calendar_service::CalendarService::new(
|
||||
calendar_storage,
|
||||
),
|
||||
);
|
||||
app_state.calendar_use_case = Some(
|
||||
calendar_service
|
||||
as Arc<dyn crate::application::ports::calendar_ports::CalendarUseCase>,
|
||||
);
|
||||
app_state.calendar_use_case = Some(calendar_service as Arc<dyn crate::application::ports::calendar_ports::CalendarUseCase>);
|
||||
|
||||
// CardDAV
|
||||
let address_book_repo: Arc<dyn crate::domain::repositories::address_book_repository::AddressBookRepository> = Arc::new(
|
||||
crate::infrastructure::repositories::pg::AddressBookPgRepository::new(pool.clone())
|
||||
let address_book_repo: Arc<
|
||||
dyn crate::domain::repositories::address_book_repository::AddressBookRepository,
|
||||
> = Arc::new(
|
||||
crate::infrastructure::repositories::pg::AddressBookPgRepository::new(pool.clone()),
|
||||
);
|
||||
let contact_repo: Arc<dyn crate::domain::repositories::contact_repository::ContactRepository> = Arc::new(
|
||||
crate::infrastructure::repositories::pg::ContactPgRepository::new(pool.clone())
|
||||
let contact_repo: Arc<
|
||||
dyn crate::domain::repositories::contact_repository::ContactRepository,
|
||||
> = Arc::new(
|
||||
crate::infrastructure::repositories::pg::ContactPgRepository::new(pool.clone()),
|
||||
);
|
||||
let group_repo: Arc<dyn crate::domain::repositories::contact_repository::ContactGroupRepository> = Arc::new(
|
||||
crate::infrastructure::repositories::pg::ContactGroupPgRepository::new(pool.clone())
|
||||
let group_repo: Arc<
|
||||
dyn crate::domain::repositories::contact_repository::ContactGroupRepository,
|
||||
> = Arc::new(
|
||||
crate::infrastructure::repositories::pg::ContactGroupPgRepository::new(
|
||||
pool.clone(),
|
||||
),
|
||||
);
|
||||
let contact_storage = Arc::new(
|
||||
crate::infrastructure::adapters::contact_storage_adapter::ContactStorageAdapter::new(
|
||||
@@ -665,8 +692,12 @@ impl AppServiceFactory {
|
||||
group_repo,
|
||||
)
|
||||
);
|
||||
app_state.addressbook_use_case = Some(contact_storage.clone() as Arc<dyn crate::application::ports::carddav_ports::AddressBookUseCase>);
|
||||
app_state.contact_use_case = Some(contact_storage as Arc<dyn crate::application::ports::carddav_ports::ContactUseCase>);
|
||||
app_state.addressbook_use_case = Some(contact_storage.clone()
|
||||
as Arc<dyn crate::application::ports::carddav_ports::AddressBookUseCase>);
|
||||
app_state.contact_use_case = Some(
|
||||
contact_storage
|
||||
as Arc<dyn crate::application::ports::carddav_ports::ContactUseCase>,
|
||||
);
|
||||
|
||||
tracing::info!("CalDAV and CardDAV services initialized with PostgreSQL repositories");
|
||||
}
|
||||
@@ -702,7 +733,8 @@ pub struct RepositoryServices {
|
||||
pub i18n_repository: Arc<dyn I18nService>,
|
||||
pub storage_mediator: Arc<dyn StorageMediator>,
|
||||
pub metadata_cache: Arc<FileMetadataCache>,
|
||||
pub trash_repository: Option<Arc<dyn crate::domain::repositories::trash_repository::TrashRepository>>,
|
||||
pub trash_repository:
|
||||
Option<Arc<dyn crate::domain::repositories::trash_repository::TrashRepository>>,
|
||||
}
|
||||
|
||||
/// Container for application services
|
||||
@@ -744,11 +776,14 @@ pub struct AppState {
|
||||
pub share_service: Option<Arc<dyn crate::application::ports::share_ports::ShareUseCase>>,
|
||||
pub favorites_service: Option<Arc<dyn FavoritesUseCase>>,
|
||||
pub recent_service: Option<Arc<dyn RecentItemsUseCase>>,
|
||||
pub storage_usage_service: Option<Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>>,
|
||||
pub storage_usage_service:
|
||||
Option<Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>>,
|
||||
pub calendar_service: Option<Arc<dyn crate::application::ports::storage_ports::StorageUseCase>>,
|
||||
pub contact_service: Option<Arc<dyn crate::application::ports::storage_ports::StorageUseCase>>,
|
||||
pub calendar_use_case: Option<Arc<dyn crate::application::ports::calendar_ports::CalendarUseCase>>,
|
||||
pub addressbook_use_case: Option<Arc<dyn crate::application::ports::carddav_ports::AddressBookUseCase>>,
|
||||
pub calendar_use_case:
|
||||
Option<Arc<dyn crate::application::ports::calendar_ports::CalendarUseCase>>,
|
||||
pub addressbook_use_case:
|
||||
Option<Arc<dyn crate::application::ports::carddav_ports::AddressBookUseCase>>,
|
||||
pub contact_use_case: Option<Arc<dyn crate::application::ports::carddav_ports::ContactUseCase>>,
|
||||
}
|
||||
|
||||
@@ -760,19 +795,27 @@ impl Default for AppState {
|
||||
let config = crate::common::config::AppConfig::default();
|
||||
let path_service = Arc::new(
|
||||
crate::infrastructure::services::path_service::PathService::new(
|
||||
std::path::PathBuf::from("./storage")
|
||||
)
|
||||
std::path::PathBuf::from("./storage"),
|
||||
),
|
||||
);
|
||||
|
||||
// Create service instances from the stubs module
|
||||
let id_mapping_service = Arc::new(StubIdMappingService) as Arc<dyn crate::application::ports::outbound::IdMappingPort>;
|
||||
let storage_mediator = Arc::new(StubStorageMediator) as Arc<dyn crate::application::services::storage_mediator::StorageMediator>;
|
||||
let i18n_repository = Arc::new(StubI18nService) as Arc<dyn crate::domain::services::i18n_service::I18nService>;
|
||||
let folder_service = Arc::new(StubFolderUseCase) as Arc<dyn crate::application::ports::inbound::FolderUseCase>;
|
||||
let file_upload_service = Arc::new(StubFileUploadUseCase) as Arc<dyn crate::application::ports::file_ports::FileUploadUseCase>;
|
||||
let file_retrieval_service = Arc::new(StubFileRetrievalUseCase) as Arc<dyn crate::application::ports::file_ports::FileRetrievalUseCase>;
|
||||
let file_management_service = Arc::new(StubFileManagementUseCase) as Arc<dyn crate::application::ports::file_ports::FileManagementUseCase>;
|
||||
let file_use_case_factory = Arc::new(StubFileUseCaseFactory) as Arc<dyn crate::application::ports::file_ports::FileUseCaseFactory>;
|
||||
let id_mapping_service = Arc::new(StubIdMappingService)
|
||||
as Arc<dyn crate::application::ports::outbound::IdMappingPort>;
|
||||
let storage_mediator = Arc::new(StubStorageMediator)
|
||||
as Arc<dyn crate::application::services::storage_mediator::StorageMediator>;
|
||||
let i18n_repository = Arc::new(StubI18nService)
|
||||
as Arc<dyn crate::domain::services::i18n_service::I18nService>;
|
||||
let folder_service = Arc::new(StubFolderUseCase)
|
||||
as Arc<dyn crate::application::ports::inbound::FolderUseCase>;
|
||||
let file_upload_service = Arc::new(StubFileUploadUseCase)
|
||||
as Arc<dyn crate::application::ports::file_ports::FileUploadUseCase>;
|
||||
let file_retrieval_service = Arc::new(StubFileRetrievalUseCase)
|
||||
as Arc<dyn crate::application::ports::file_ports::FileRetrievalUseCase>;
|
||||
let file_management_service = Arc::new(StubFileManagementUseCase)
|
||||
as Arc<dyn crate::application::ports::file_ports::FileManagementUseCase>;
|
||||
let file_use_case_factory = Arc::new(StubFileUseCaseFactory)
|
||||
as Arc<dyn crate::application::ports::file_ports::FileUseCaseFactory>;
|
||||
|
||||
// Create dummy ID mapping service for files
|
||||
let dummy_file_id_mapping = Arc::new(IdMappingService::dummy());
|
||||
@@ -787,17 +830,18 @@ impl Default for AppState {
|
||||
&std::path::PathBuf::from("./storage"),
|
||||
100,
|
||||
10 * 1024 * 1024,
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
// Create dummy write-behind cache
|
||||
let dummy_write_behind_cache: Arc<dyn WriteBehindCachePort> = crate::infrastructure::services::write_behind_cache::WriteBehindCache::new();
|
||||
let dummy_write_behind_cache: Arc<dyn WriteBehindCachePort> =
|
||||
crate::infrastructure::services::write_behind_cache::WriteBehindCache::new();
|
||||
|
||||
// Create dummy chunked upload service
|
||||
let dummy_chunked_upload_service: Arc<dyn ChunkedUploadPort> = Arc::new(
|
||||
crate::infrastructure::services::chunked_upload_service::ChunkedUploadService::new(
|
||||
std::path::PathBuf::from("./storage/.uploads")
|
||||
)
|
||||
std::path::PathBuf::from("./storage/.uploads"),
|
||||
),
|
||||
);
|
||||
|
||||
// Create dummy image transcode service
|
||||
@@ -806,14 +850,14 @@ impl Default for AppState {
|
||||
&std::path::PathBuf::from("./storage"),
|
||||
100,
|
||||
10 * 1024 * 1024,
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
// Create dummy dedup service
|
||||
let dummy_dedup_service: Arc<dyn DedupPort> = Arc::new(
|
||||
crate::infrastructure::services::dedup_service::DedupService::new(
|
||||
&std::path::PathBuf::from("./storage")
|
||||
)
|
||||
&std::path::PathBuf::from("./storage"),
|
||||
),
|
||||
);
|
||||
|
||||
// Core services using stubs
|
||||
@@ -838,9 +882,12 @@ impl Default for AppState {
|
||||
|
||||
// Repository services using stubs
|
||||
let repository_services = RepositoryServices {
|
||||
folder_repository: Arc::new(StubFolderStoragePort) as Arc<dyn crate::application::ports::outbound::FolderStoragePort>,
|
||||
file_read_repository: Arc::new(StubFileReadPort) as Arc<dyn crate::application::ports::storage_ports::FileReadPort>,
|
||||
file_write_repository: Arc::new(StubFileWritePort) as Arc<dyn crate::application::ports::storage_ports::FileWritePort>,
|
||||
folder_repository: Arc::new(StubFolderStoragePort)
|
||||
as Arc<dyn crate::application::ports::outbound::FolderStoragePort>,
|
||||
file_read_repository: Arc::new(StubFileReadPort)
|
||||
as Arc<dyn crate::application::ports::storage_ports::FileReadPort>,
|
||||
file_write_repository: Arc::new(StubFileWritePort)
|
||||
as Arc<dyn crate::application::ports::storage_ports::FileWritePort>,
|
||||
i18n_repository,
|
||||
storage_mediator: storage_mediator.clone(),
|
||||
metadata_cache: dummy_metadata_cache,
|
||||
@@ -848,12 +895,15 @@ impl Default for AppState {
|
||||
};
|
||||
|
||||
// Dummy concrete services for compatibility
|
||||
let dummy_folder_storage = Arc::new(StubFolderStoragePort) as Arc<dyn crate::application::ports::outbound::FolderStoragePort>;
|
||||
let dummy_folder_storage = Arc::new(StubFolderStoragePort)
|
||||
as Arc<dyn crate::application::ports::outbound::FolderStoragePort>;
|
||||
let folder_service_concrete = Arc::new(FolderService::new(dummy_folder_storage));
|
||||
|
||||
// Dummy I18nApplicationService
|
||||
let dummy_i18n_app_service = crate::application::services::i18n_application_service::I18nApplicationService::new(
|
||||
Arc::new(StubI18nService) as Arc<dyn crate::domain::services::i18n_service::I18nService>
|
||||
let dummy_i18n_app_service =
|
||||
crate::application::services::i18n_application_service::I18nApplicationService::new(
|
||||
Arc::new(StubI18nService)
|
||||
as Arc<dyn crate::domain::services::i18n_service::I18nService>,
|
||||
);
|
||||
|
||||
// Application services using stubs
|
||||
@@ -866,7 +916,8 @@ impl Default for AppState {
|
||||
file_use_case_factory,
|
||||
i18n_service: Arc::new(dummy_i18n_app_service),
|
||||
trash_service: None,
|
||||
search_service: Some(Arc::new(StubSearchUseCase) as Arc<dyn crate::application::ports::inbound::SearchUseCase>),
|
||||
search_service: Some(Arc::new(StubSearchUseCase)
|
||||
as Arc<dyn crate::application::ports::inbound::SearchUseCase>),
|
||||
share_service: None,
|
||||
favorites_service: None,
|
||||
recent_service: None,
|
||||
@@ -931,11 +982,15 @@ impl AppState {
|
||||
/// This keeps `routes.rs` free of any `crate::infrastructure` references.
|
||||
pub fn for_routing(
|
||||
folder_service: Arc<FolderService>,
|
||||
file_retrieval_service: Arc<dyn crate::application::ports::file_ports::FileRetrievalUseCase>,
|
||||
file_retrieval_service: Arc<
|
||||
dyn crate::application::ports::file_ports::FileRetrievalUseCase,
|
||||
>,
|
||||
file_upload_service: Arc<dyn FileUploadUseCase>,
|
||||
file_management_service: Arc<dyn FileManagementUseCase>,
|
||||
folder_use_case: Arc<dyn crate::application::ports::inbound::FolderUseCase>,
|
||||
i18n_service: Option<Arc<crate::application::services::i18n_application_service::I18nApplicationService>>,
|
||||
i18n_service: Option<
|
||||
Arc<crate::application::services::i18n_application_service::I18nApplicationService>,
|
||||
>,
|
||||
trash_service: Option<Arc<dyn TrashUseCase>>,
|
||||
search_service: Option<Arc<dyn crate::application::ports::inbound::SearchUseCase>>,
|
||||
share_service: Option<Arc<dyn crate::application::ports::share_ports::ShareUseCase>>,
|
||||
@@ -970,9 +1025,11 @@ impl AppState {
|
||||
// Create real ZipService with the actual file/folder services
|
||||
state.core.zip_service = Arc::new(
|
||||
crate::infrastructure::services::zip_service::ZipService::new(
|
||||
file_retrieval_service as Arc<dyn crate::application::ports::file_ports::FileRetrievalUseCase>,
|
||||
folder_service.clone() as Arc<dyn crate::application::ports::inbound::FolderUseCase>,
|
||||
)
|
||||
file_retrieval_service
|
||||
as Arc<dyn crate::application::ports::file_ports::FileRetrievalUseCase>,
|
||||
folder_service.clone()
|
||||
as Arc<dyn crate::application::ports::inbound::FolderUseCase>,
|
||||
),
|
||||
);
|
||||
|
||||
state
|
||||
@@ -988,7 +1045,10 @@ impl AppState {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_share_service(mut self, share_service: Arc<dyn crate::application::ports::share_ports::ShareUseCase>) -> Self {
|
||||
pub fn with_share_service(
|
||||
mut self,
|
||||
share_service: Arc<dyn crate::application::ports::share_ports::ShareUseCase>,
|
||||
) -> Self {
|
||||
self.share_service = Some(share_service);
|
||||
self
|
||||
}
|
||||
@@ -1003,32 +1063,50 @@ impl AppState {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_storage_usage_service(mut self, storage_usage_service: Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>) -> Self {
|
||||
pub fn with_storage_usage_service(
|
||||
mut self,
|
||||
storage_usage_service: Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>,
|
||||
) -> Self {
|
||||
self.storage_usage_service = Some(storage_usage_service);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_calendar_service(mut self, calendar_service: Arc<dyn crate::application::ports::storage_ports::StorageUseCase>) -> Self {
|
||||
pub fn with_calendar_service(
|
||||
mut self,
|
||||
calendar_service: Arc<dyn crate::application::ports::storage_ports::StorageUseCase>,
|
||||
) -> Self {
|
||||
self.calendar_service = Some(calendar_service);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_contact_service(mut self, contact_service: Arc<dyn crate::application::ports::storage_ports::StorageUseCase>) -> Self {
|
||||
pub fn with_contact_service(
|
||||
mut self,
|
||||
contact_service: Arc<dyn crate::application::ports::storage_ports::StorageUseCase>,
|
||||
) -> Self {
|
||||
self.contact_service = Some(contact_service);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_calendar_use_case(mut self, calendar_use_case: Arc<dyn crate::application::ports::calendar_ports::CalendarUseCase>) -> Self {
|
||||
pub fn with_calendar_use_case(
|
||||
mut self,
|
||||
calendar_use_case: Arc<dyn crate::application::ports::calendar_ports::CalendarUseCase>,
|
||||
) -> Self {
|
||||
self.calendar_use_case = Some(calendar_use_case);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_addressbook_use_case(mut self, addressbook_use_case: Arc<dyn crate::application::ports::carddav_ports::AddressBookUseCase>) -> Self {
|
||||
pub fn with_addressbook_use_case(
|
||||
mut self,
|
||||
addressbook_use_case: Arc<dyn crate::application::ports::carddav_ports::AddressBookUseCase>,
|
||||
) -> Self {
|
||||
self.addressbook_use_case = Some(addressbook_use_case);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_contact_use_case(mut self, contact_use_case: Arc<dyn crate::application::ports::carddav_ports::ContactUseCase>) -> Self {
|
||||
pub fn with_contact_use_case(
|
||||
mut self,
|
||||
contact_use_case: Arc<dyn crate::application::ports::carddav_ports::ContactUseCase>,
|
||||
) -> Self {
|
||||
self.contact_use_case = Some(contact_use_case);
|
||||
self
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
pub mod errors;
|
||||
pub mod config;
|
||||
pub mod di;
|
||||
pub mod errors;
|
||||
pub mod stubs;
|
||||
+52
-103
@@ -14,23 +14,25 @@ use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
|
||||
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::search_dto::{SearchCriteriaDto, SearchResultsDto};
|
||||
use crate::application::ports::compression_ports::{CompressionLevel, CompressionPort};
|
||||
use crate::application::ports::file_ports::{
|
||||
FileManagementUseCase, FileRetrievalUseCase, FileUploadUseCase, FileUseCaseFactory,
|
||||
UploadStrategy, OptimizedFileContent,
|
||||
OptimizedFileContent, UploadStrategy,
|
||||
};
|
||||
use crate::application::ports::inbound::{FolderUseCase, SearchUseCase};
|
||||
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::zip_ports::ZipPort;
|
||||
use crate::application::services::storage_mediator::{StorageMediator, StorageMediatorError};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::file::File;
|
||||
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::path_service::StoragePath;
|
||||
|
||||
@@ -89,10 +91,7 @@ pub struct StubIdMappingService;
|
||||
|
||||
#[async_trait]
|
||||
impl IdMappingPort for StubIdMappingService {
|
||||
async fn get_or_create_id(
|
||||
&self,
|
||||
_path: &StoragePath,
|
||||
) -> Result<String, DomainError> {
|
||||
async fn get_or_create_id(&self, _path: &StoragePath) -> Result<String, DomainError> {
|
||||
Ok("dummy-id".to_string())
|
||||
}
|
||||
|
||||
@@ -100,11 +99,7 @@ impl IdMappingPort for StubIdMappingService {
|
||||
Ok(StoragePath::from_string("/"))
|
||||
}
|
||||
|
||||
async fn update_path(
|
||||
&self,
|
||||
_id: &str,
|
||||
_new_path: &StoragePath,
|
||||
) -> Result<(), DomainError> {
|
||||
async fn update_path(&self, _id: &str, _new_path: &StoragePath) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -125,10 +120,7 @@ pub struct StubStorageMediator;
|
||||
|
||||
#[async_trait]
|
||||
impl StorageMediator for StubStorageMediator {
|
||||
async fn get_folder_path(
|
||||
&self,
|
||||
_folder_id: &str,
|
||||
) -> Result<PathBuf, StorageMediatorError> {
|
||||
async fn get_folder_path(&self, _folder_id: &str) -> Result<PathBuf, StorageMediatorError> {
|
||||
Ok(PathBuf::from("/tmp"))
|
||||
}
|
||||
|
||||
@@ -139,19 +131,13 @@ impl StorageMediator for StubStorageMediator {
|
||||
Ok(StoragePath::root())
|
||||
}
|
||||
|
||||
async fn get_folder(
|
||||
&self,
|
||||
_folder_id: &str,
|
||||
) -> Result<Folder, StorageMediatorError> {
|
||||
async fn get_folder(&self, _folder_id: &str) -> Result<Folder, StorageMediatorError> {
|
||||
Err(StorageMediatorError::NotFound(
|
||||
"Stub not implemented".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn file_exists_at_path(
|
||||
&self,
|
||||
_path: &Path,
|
||||
) -> Result<bool, StorageMediatorError> {
|
||||
async fn file_exists_at_path(&self, _path: &Path) -> Result<bool, StorageMediatorError> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
@@ -162,10 +148,7 @@ impl StorageMediator for StubStorageMediator {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn folder_exists_at_path(
|
||||
&self,
|
||||
_path: &Path,
|
||||
) -> Result<bool, StorageMediatorError> {
|
||||
async fn folder_exists_at_path(&self, _path: &Path) -> Result<bool, StorageMediatorError> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
@@ -184,10 +167,7 @@ impl StorageMediator for StubStorageMediator {
|
||||
PathBuf::from("/tmp")
|
||||
}
|
||||
|
||||
async fn ensure_directory(
|
||||
&self,
|
||||
_path: &Path,
|
||||
) -> Result<(), StorageMediatorError> {
|
||||
async fn ensure_directory(&self, _path: &Path) -> Result<(), StorageMediatorError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -236,10 +216,7 @@ impl FileReadPort for StubFileReadPort {
|
||||
Ok(File::default())
|
||||
}
|
||||
|
||||
async fn list_files(
|
||||
&self,
|
||||
_folder_id: Option<&str>,
|
||||
) -> Result<Vec<File>, DomainError> {
|
||||
async fn list_files(&self, _folder_id: Option<&str>) -> Result<Vec<File>, DomainError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
@@ -314,11 +291,7 @@ impl FileWritePort for StubFileWritePort {
|
||||
Ok(File::default())
|
||||
}
|
||||
|
||||
async fn rename_file(
|
||||
&self,
|
||||
_file_id: &str,
|
||||
_new_name: &str,
|
||||
) -> Result<File, DomainError> {
|
||||
async fn rename_file(&self, _file_id: &str, _new_name: &str) -> Result<File, DomainError> {
|
||||
Ok(File::default())
|
||||
}
|
||||
|
||||
@@ -348,7 +321,11 @@ impl FileWritePort for StubFileWritePort {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -377,17 +354,11 @@ impl FolderRepository for StubFolderStoragePort {
|
||||
Ok(Folder::default())
|
||||
}
|
||||
|
||||
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> {
|
||||
Ok(Folder::default())
|
||||
}
|
||||
|
||||
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> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
@@ -401,11 +372,7 @@ impl FolderRepository for StubFolderStoragePort {
|
||||
Ok((Vec::new(), Some(0)))
|
||||
}
|
||||
|
||||
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> {
|
||||
Ok(Folder::default())
|
||||
}
|
||||
|
||||
@@ -421,17 +388,11 @@ impl FolderRepository for StubFolderStoragePort {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn folder_exists(
|
||||
&self,
|
||||
_storage_path: &StoragePath,
|
||||
) -> Result<bool, DomainError> {
|
||||
async fn folder_exists(&self, _storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn get_folder_path(
|
||||
&self,
|
||||
_id: &str,
|
||||
) -> Result<StoragePath, DomainError> {
|
||||
async fn get_folder_path(&self, _id: &str) -> Result<StoragePath, DomainError> {
|
||||
Ok(StoragePath::from_string("/"))
|
||||
}
|
||||
|
||||
@@ -439,7 +400,11 @@ impl FolderRepository for StubFolderStoragePort {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -481,10 +446,7 @@ pub struct StubFolderUseCase;
|
||||
|
||||
#[async_trait]
|
||||
impl FolderUseCase for StubFolderUseCase {
|
||||
async fn create_folder(
|
||||
&self,
|
||||
_dto: CreateFolderDto,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
async fn create_folder(&self, _dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
|
||||
Ok(FolderDto::default())
|
||||
}
|
||||
|
||||
@@ -492,17 +454,11 @@ impl FolderUseCase for StubFolderUseCase {
|
||||
Ok(FolderDto::default())
|
||||
}
|
||||
|
||||
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::default())
|
||||
}
|
||||
|
||||
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::new())
|
||||
}
|
||||
|
||||
@@ -522,11 +478,7 @@ impl FolderUseCase for StubFolderUseCase {
|
||||
Ok(FolderDto::default())
|
||||
}
|
||||
|
||||
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::default())
|
||||
}
|
||||
|
||||
@@ -564,7 +516,13 @@ impl FileUploadUseCase for StubFileUploadUseCase {
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -585,10 +543,7 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
|
||||
Ok(FileDto::default())
|
||||
}
|
||||
|
||||
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> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
@@ -610,11 +565,14 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
|
||||
_accept_webp: bool,
|
||||
_prefer_original: bool,
|
||||
) -> Result<(FileDto, OptimizedFileContent), DomainError> {
|
||||
Ok((FileDto::default(), OptimizedFileContent::Bytes {
|
||||
Ok((
|
||||
FileDto::default(),
|
||||
OptimizedFileContent::Bytes {
|
||||
data: Bytes::new(),
|
||||
mime_type: String::new(),
|
||||
was_transcoded: false,
|
||||
}))
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_file_range_stream(
|
||||
@@ -648,11 +606,7 @@ impl FileManagementUseCase for StubFileManagementUseCase {
|
||||
Ok(FileDto::default())
|
||||
}
|
||||
|
||||
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> {
|
||||
Ok(FileDto::default())
|
||||
}
|
||||
|
||||
@@ -660,11 +614,7 @@ impl FileManagementUseCase for StubFileManagementUseCase {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_with_cleanup(
|
||||
&self,
|
||||
_id: &str,
|
||||
_user_id: &str,
|
||||
) -> Result<bool, DomainError> {
|
||||
async fn delete_with_cleanup(&self, _id: &str, _user_id: &str) -> Result<bool, DomainError> {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
@@ -697,10 +647,7 @@ pub struct StubSearchUseCase;
|
||||
|
||||
#[async_trait]
|
||||
impl SearchUseCase for StubSearchUseCase {
|
||||
async fn search(
|
||||
&self,
|
||||
_criteria: SearchCriteriaDto,
|
||||
) -> Result<SearchResultsDto, DomainError> {
|
||||
async fn search(&self, _criteria: SearchCriteriaDto) -> Result<SearchResultsDto, DomainError> {
|
||||
Ok(SearchResultsDto::empty())
|
||||
}
|
||||
|
||||
@@ -713,7 +660,9 @@ impl SearchUseCase for StubSearchUseCase {
|
||||
// MetadataCachePort
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use crate::application::ports::cache_ports::{MetadataCachePort, CachedMetadataDto, ContentCachePort};
|
||||
use crate::application::ports::cache_ports::{
|
||||
CachedMetadataDto, ContentCachePort, MetadataCachePort,
|
||||
};
|
||||
|
||||
pub struct StubMetadataCachePort;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
/**
|
||||
* Calendar Entity
|
||||
*
|
||||
@@ -8,11 +9,9 @@
|
||||
* containers for calendar events. Each calendar belongs to a specific user and can
|
||||
* have custom properties.
|
||||
*/
|
||||
|
||||
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
|
||||
pub use super::entity_errors::CalendarError;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use chrono::{DateTime, Duration, TimeZone, Utc};
|
||||
/**
|
||||
* Calendar Event Entity
|
||||
*
|
||||
@@ -8,11 +9,9 @@
|
||||
* and can include recurrence rules for repeating events. Each event belongs to a
|
||||
* specific calendar and stores its complete iCalendar representation.
|
||||
*/
|
||||
|
||||
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
|
||||
pub use super::entity_errors::CalendarEventError;
|
||||
@@ -110,7 +109,8 @@ impl CalendarEvent {
|
||||
|
||||
// Validate RRULE if provided (basic validation)
|
||||
if let Some(ref rule) = rrule
|
||||
&& !rule.starts_with("FREQ=") {
|
||||
&& !rule.starts_with("FREQ=")
|
||||
{
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"CalendarEvent",
|
||||
@@ -227,41 +227,46 @@ impl CalendarEvent {
|
||||
// For brevity, we're using a simplified version here
|
||||
|
||||
// Extract required fields from iCalendar data
|
||||
let summary = Self::extract_ical_property(&ical_data, "SUMMARY")
|
||||
.ok_or_else(|| DomainError::new(
|
||||
let summary = Self::extract_ical_property(&ical_data, "SUMMARY").ok_or_else(|| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"CalendarEvent",
|
||||
"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,
|
||||
"CalendarEvent",
|
||||
"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,
|
||||
"CalendarEvent",
|
||||
"Missing DTEND in iCalendar data",
|
||||
))?;
|
||||
)
|
||||
})?;
|
||||
|
||||
// Parse dates (simplified)
|
||||
let start_time = Self::parse_ical_datetime(&dtstart)
|
||||
.map_err(|e| DomainError::new(
|
||||
let start_time = Self::parse_ical_datetime(&dtstart).map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"CalendarEvent",
|
||||
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,
|
||||
"CalendarEvent",
|
||||
format!("Invalid DTEND: {}", e),
|
||||
))?;
|
||||
)
|
||||
})?;
|
||||
|
||||
// Determine if all-day event (simplified check)
|
||||
let all_day = dtstart.contains("VALUE=DATE") && !dtstart.contains("T");
|
||||
@@ -433,7 +438,11 @@ impl CalendarEvent {
|
||||
* @param end_time New end time for the event
|
||||
* @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 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
@@ -500,7 +509,8 @@ impl CalendarEvent {
|
||||
pub fn update_rrule(&mut self, rrule: Option<String>) -> Result<()> {
|
||||
// Validate RRULE if provided (basic validation)
|
||||
if let Some(ref rule) = rrule
|
||||
&& !rule.starts_with("FREQ=") {
|
||||
&& !rule.starts_with("FREQ=")
|
||||
{
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"CalendarEvent",
|
||||
@@ -546,12 +556,14 @@ impl CalendarEvent {
|
||||
self.location = Self::extract_ical_property(&ical_data, "LOCATION");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -607,7 +619,7 @@ impl CalendarEvent {
|
||||
if let Some(until_pos) = rrule.find("UNTIL=") {
|
||||
let until_start = until_pos + 6; // "UNTIL=" is 6 chars
|
||||
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) {
|
||||
return until_date >= *start;
|
||||
}
|
||||
@@ -641,7 +653,8 @@ impl CalendarEvent {
|
||||
let search_str = format!("\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));
|
||||
|
||||
if let Some(pos) = pos {
|
||||
@@ -678,11 +691,14 @@ impl CalendarEvent {
|
||||
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())?;
|
||||
let month = date_str[4..6].parse::<u32>()
|
||||
let month = date_str[4..6]
|
||||
.parse::<u32>()
|
||||
.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())?;
|
||||
|
||||
return match chrono::NaiveDate::from_ymd_opt(year, month, day) {
|
||||
@@ -697,18 +713,24 @@ impl CalendarEvent {
|
||||
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())?;
|
||||
let month = datetime_str[4..6].parse::<u32>()
|
||||
let month = datetime_str[4..6]
|
||||
.parse::<u32>()
|
||||
.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())?;
|
||||
|
||||
let hour = datetime_str[9..11].parse::<u32>()
|
||||
let hour = datetime_str[9..11]
|
||||
.parse::<u32>()
|
||||
.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())?;
|
||||
let second = datetime_str[13..15].parse::<u32>()
|
||||
let second = datetime_str[13..15]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| "Invalid second".to_string())?;
|
||||
|
||||
match chrono::NaiveDate::from_ymd_opt(year, month, day) {
|
||||
@@ -731,7 +753,9 @@ impl CalendarEvent {
|
||||
let search_str_alt = format!("\r\n{}:", property_name);
|
||||
|
||||
// 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));
|
||||
|
||||
if let Some(pos) = pos {
|
||||
@@ -750,7 +774,9 @@ impl CalendarEvent {
|
||||
self.ical_data = format!("{}{}{}", before, value, after);
|
||||
} else {
|
||||
// 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());
|
||||
|
||||
let before = &self.ical_data[..end_pos];
|
||||
@@ -769,7 +795,9 @@ impl CalendarEvent {
|
||||
let search_str_alt = format!("\r\n{}:", property_name);
|
||||
|
||||
// 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));
|
||||
|
||||
if let Some(pos) = pos {
|
||||
|
||||
+262
-77
@@ -46,25 +46,64 @@ impl AddressBook {
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
) -> 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 ---
|
||||
pub fn id(&self) -> &Uuid { &self.id }
|
||||
pub fn name(&self) -> &str { &self.name }
|
||||
pub fn owner_id(&self) -> &str { &self.owner_id }
|
||||
pub fn description(&self) -> Option<&str> { self.description.as_deref() }
|
||||
pub fn color(&self) -> Option<&str> { self.color.as_deref() }
|
||||
pub fn is_public(&self) -> bool { self.is_public }
|
||||
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
|
||||
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
|
||||
pub fn id(&self) -> &Uuid {
|
||||
&self.id
|
||||
}
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
pub fn owner_id(&self) -> &str {
|
||||
&self.owner_id
|
||||
}
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
self.description.as_deref()
|
||||
}
|
||||
pub fn color(&self) -> Option<&str> {
|
||||
self.color.as_deref()
|
||||
}
|
||||
pub fn is_public(&self) -> bool {
|
||||
self.is_public
|
||||
}
|
||||
pub fn created_at(&self) -> &DateTime<Utc> {
|
||||
&self.created_at
|
||||
}
|
||||
pub fn updated_at(&self) -> &DateTime<Utc> {
|
||||
&self.updated_at
|
||||
}
|
||||
|
||||
// --- Setters for mutable operations ---
|
||||
pub fn set_name(&mut self, name: String) { self.name = name; self.updated_at = Utc::now(); }
|
||||
pub fn set_description(&mut self, description: Option<String>) { self.description = description; self.updated_at = Utc::now(); }
|
||||
pub fn set_color(&mut self, color: Option<String>) { self.color = color; self.updated_at = Utc::now(); }
|
||||
pub fn set_is_public(&mut self, is_public: bool) { self.is_public = is_public; self.updated_at = Utc::now(); }
|
||||
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
|
||||
pub fn set_name(&mut self, name: String) {
|
||||
self.name = name;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
pub fn set_description(&mut self, description: Option<String>) {
|
||||
self.description = description;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
pub fn set_color(&mut self, color: Option<String>) {
|
||||
self.color = color;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
pub fn set_is_public(&mut self, is_public: bool) {
|
||||
self.is_public = is_public;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) {
|
||||
self.updated_at = updated_at;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AddressBook {
|
||||
@@ -198,72 +237,191 @@ impl Contact {
|
||||
updated_at: DateTime<Utc>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id, address_book_id, uid, full_name, first_name, last_name, nickname,
|
||||
email, phone, address, organization, title, notes, photo_url,
|
||||
birthday, anniversary, vcard, etag, created_at, updated_at,
|
||||
id,
|
||||
address_book_id,
|
||||
uid,
|
||||
full_name,
|
||||
first_name,
|
||||
last_name,
|
||||
nickname,
|
||||
email,
|
||||
phone,
|
||||
address,
|
||||
organization,
|
||||
title,
|
||||
notes,
|
||||
photo_url,
|
||||
birthday,
|
||||
anniversary,
|
||||
vcard,
|
||||
etag,
|
||||
created_at,
|
||||
updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Getters ---
|
||||
pub fn id(&self) -> &Uuid { &self.id }
|
||||
pub fn address_book_id(&self) -> &Uuid { &self.address_book_id }
|
||||
pub fn uid(&self) -> &str { &self.uid }
|
||||
pub fn full_name(&self) -> Option<&str> { self.full_name.as_deref() }
|
||||
pub fn first_name(&self) -> Option<&str> { self.first_name.as_deref() }
|
||||
pub fn last_name(&self) -> Option<&str> { self.last_name.as_deref() }
|
||||
pub fn nickname(&self) -> Option<&str> { self.nickname.as_deref() }
|
||||
pub fn email(&self) -> &[Email] { &self.email }
|
||||
pub fn phone(&self) -> &[Phone] { &self.phone }
|
||||
pub fn address(&self) -> &[Address] { &self.address }
|
||||
pub fn organization(&self) -> Option<&str> { self.organization.as_deref() }
|
||||
pub fn title(&self) -> Option<&str> { self.title.as_deref() }
|
||||
pub fn notes(&self) -> Option<&str> { self.notes.as_deref() }
|
||||
pub fn photo_url(&self) -> Option<&str> { self.photo_url.as_deref() }
|
||||
pub fn birthday(&self) -> Option<&NaiveDate> { self.birthday.as_ref() }
|
||||
pub fn anniversary(&self) -> Option<&NaiveDate> { self.anniversary.as_ref() }
|
||||
pub fn vcard(&self) -> &str { &self.vcard }
|
||||
pub fn etag(&self) -> &str { &self.etag }
|
||||
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
|
||||
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
|
||||
pub fn id(&self) -> &Uuid {
|
||||
&self.id
|
||||
}
|
||||
pub fn address_book_id(&self) -> &Uuid {
|
||||
&self.address_book_id
|
||||
}
|
||||
pub fn uid(&self) -> &str {
|
||||
&self.uid
|
||||
}
|
||||
pub fn full_name(&self) -> Option<&str> {
|
||||
self.full_name.as_deref()
|
||||
}
|
||||
pub fn first_name(&self) -> Option<&str> {
|
||||
self.first_name.as_deref()
|
||||
}
|
||||
pub fn last_name(&self) -> Option<&str> {
|
||||
self.last_name.as_deref()
|
||||
}
|
||||
pub fn nickname(&self) -> Option<&str> {
|
||||
self.nickname.as_deref()
|
||||
}
|
||||
pub fn email(&self) -> &[Email] {
|
||||
&self.email
|
||||
}
|
||||
pub fn phone(&self) -> &[Phone] {
|
||||
&self.phone
|
||||
}
|
||||
pub fn address(&self) -> &[Address] {
|
||||
&self.address
|
||||
}
|
||||
pub fn organization(&self) -> Option<&str> {
|
||||
self.organization.as_deref()
|
||||
}
|
||||
pub fn title(&self) -> Option<&str> {
|
||||
self.title.as_deref()
|
||||
}
|
||||
pub fn notes(&self) -> Option<&str> {
|
||||
self.notes.as_deref()
|
||||
}
|
||||
pub fn photo_url(&self) -> Option<&str> {
|
||||
self.photo_url.as_deref()
|
||||
}
|
||||
pub fn birthday(&self) -> Option<&NaiveDate> {
|
||||
self.birthday.as_ref()
|
||||
}
|
||||
pub fn anniversary(&self) -> Option<&NaiveDate> {
|
||||
self.anniversary.as_ref()
|
||||
}
|
||||
pub fn vcard(&self) -> &str {
|
||||
&self.vcard
|
||||
}
|
||||
pub fn etag(&self) -> &str {
|
||||
&self.etag
|
||||
}
|
||||
pub fn created_at(&self) -> &DateTime<Utc> {
|
||||
&self.created_at
|
||||
}
|
||||
pub fn updated_at(&self) -> &DateTime<Utc> {
|
||||
&self.updated_at
|
||||
}
|
||||
|
||||
// --- Owned getters for persistence layer bind() calls ---
|
||||
pub fn full_name_owned(&self) -> Option<String> { self.full_name.clone() }
|
||||
pub fn first_name_owned(&self) -> Option<String> { self.first_name.clone() }
|
||||
pub fn last_name_owned(&self) -> Option<String> { self.last_name.clone() }
|
||||
pub fn nickname_owned(&self) -> Option<String> { self.nickname.clone() }
|
||||
pub fn organization_owned(&self) -> Option<String> { self.organization.clone() }
|
||||
pub fn title_owned(&self) -> Option<String> { self.title.clone() }
|
||||
pub fn notes_owned(&self) -> Option<String> { self.notes.clone() }
|
||||
pub fn photo_url_owned(&self) -> Option<String> { self.photo_url.clone() }
|
||||
pub fn full_name_owned(&self) -> Option<String> {
|
||||
self.full_name.clone()
|
||||
}
|
||||
pub fn first_name_owned(&self) -> Option<String> {
|
||||
self.first_name.clone()
|
||||
}
|
||||
pub fn last_name_owned(&self) -> Option<String> {
|
||||
self.last_name.clone()
|
||||
}
|
||||
pub fn nickname_owned(&self) -> Option<String> {
|
||||
self.nickname.clone()
|
||||
}
|
||||
pub fn organization_owned(&self) -> Option<String> {
|
||||
self.organization.clone()
|
||||
}
|
||||
pub fn title_owned(&self) -> Option<String> {
|
||||
self.title.clone()
|
||||
}
|
||||
pub fn notes_owned(&self) -> Option<String> {
|
||||
self.notes.clone()
|
||||
}
|
||||
pub fn photo_url_owned(&self) -> Option<String> {
|
||||
self.photo_url.clone()
|
||||
}
|
||||
|
||||
// --- Setters for mutable operations (contact_service.rs needs these) ---
|
||||
pub fn set_full_name(&mut self, v: Option<String>) { self.full_name = v; }
|
||||
pub fn set_first_name(&mut self, v: Option<String>) { self.first_name = v; }
|
||||
pub fn set_last_name(&mut self, v: Option<String>) { self.last_name = v; }
|
||||
pub fn set_nickname(&mut self, v: Option<String>) { self.nickname = v; }
|
||||
pub fn set_organization(&mut self, v: Option<String>) { self.organization = v; }
|
||||
pub fn set_title(&mut self, v: Option<String>) { self.title = v; }
|
||||
pub fn set_notes(&mut self, v: Option<String>) { self.notes = v; }
|
||||
pub fn set_photo_url(&mut self, v: Option<String>) { self.photo_url = v; }
|
||||
pub fn set_birthday(&mut self, v: Option<NaiveDate>) { self.birthday = v; }
|
||||
pub fn set_anniversary(&mut self, v: Option<NaiveDate>) { self.anniversary = v; }
|
||||
pub fn set_vcard(&mut self, vcard: String) { self.vcard = vcard; }
|
||||
pub fn set_etag(&mut self, etag: String) { self.etag = etag; }
|
||||
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
|
||||
pub fn set_address_book_id(&mut self, id: Uuid) { self.address_book_id = id; }
|
||||
pub fn set_uid(&mut self, uid: String) { self.uid = uid; }
|
||||
pub fn set_full_name(&mut self, v: Option<String>) {
|
||||
self.full_name = v;
|
||||
}
|
||||
pub fn set_first_name(&mut self, v: Option<String>) {
|
||||
self.first_name = v;
|
||||
}
|
||||
pub fn set_last_name(&mut self, v: Option<String>) {
|
||||
self.last_name = v;
|
||||
}
|
||||
pub fn set_nickname(&mut self, v: Option<String>) {
|
||||
self.nickname = v;
|
||||
}
|
||||
pub fn set_organization(&mut self, v: Option<String>) {
|
||||
self.organization = v;
|
||||
}
|
||||
pub fn set_title(&mut self, v: Option<String>) {
|
||||
self.title = v;
|
||||
}
|
||||
pub fn set_notes(&mut self, v: Option<String>) {
|
||||
self.notes = v;
|
||||
}
|
||||
pub fn set_photo_url(&mut self, v: Option<String>) {
|
||||
self.photo_url = v;
|
||||
}
|
||||
pub fn set_birthday(&mut self, v: Option<NaiveDate>) {
|
||||
self.birthday = v;
|
||||
}
|
||||
pub fn set_anniversary(&mut self, v: Option<NaiveDate>) {
|
||||
self.anniversary = v;
|
||||
}
|
||||
pub fn set_vcard(&mut self, vcard: String) {
|
||||
self.vcard = vcard;
|
||||
}
|
||||
pub fn set_etag(&mut self, etag: String) {
|
||||
self.etag = etag;
|
||||
}
|
||||
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) {
|
||||
self.updated_at = updated_at;
|
||||
}
|
||||
pub fn set_address_book_id(&mut self, id: Uuid) {
|
||||
self.address_book_id = id;
|
||||
}
|
||||
pub fn set_uid(&mut self, uid: String) {
|
||||
self.uid = uid;
|
||||
}
|
||||
|
||||
// --- Collection mutators ---
|
||||
pub fn push_email(&mut self, e: Email) { self.email.push(e); }
|
||||
pub fn push_phone(&mut self, p: Phone) { self.phone.push(p); }
|
||||
pub fn set_email(&mut self, email: Vec<Email>) { self.email = email; }
|
||||
pub fn set_phone(&mut self, phone: Vec<Phone>) { self.phone = phone; }
|
||||
pub fn set_address(&mut self, address: Vec<Address>) { self.address = address; }
|
||||
pub fn email_is_empty(&self) -> bool { self.email.is_empty() }
|
||||
pub fn phone_is_empty(&self) -> bool { self.phone.is_empty() }
|
||||
pub fn push_email(&mut self, e: Email) {
|
||||
self.email.push(e);
|
||||
}
|
||||
pub fn push_phone(&mut self, p: Phone) {
|
||||
self.phone.push(p);
|
||||
}
|
||||
pub fn set_email(&mut self, email: Vec<Email>) {
|
||||
self.email = email;
|
||||
}
|
||||
pub fn set_phone(&mut self, phone: Vec<Phone>) {
|
||||
self.phone = phone;
|
||||
}
|
||||
pub fn set_address(&mut self, address: Vec<Address>) {
|
||||
self.address = address;
|
||||
}
|
||||
pub fn email_is_empty(&self) -> bool {
|
||||
self.email.is_empty()
|
||||
}
|
||||
pub fn phone_is_empty(&self) -> bool {
|
||||
self.phone.is_empty()
|
||||
}
|
||||
|
||||
// --- Consuming methods for ownership transfer ---
|
||||
pub fn into_email(self) -> Vec<Email> { self.email }
|
||||
pub fn into_email(self) -> Vec<Email> {
|
||||
self.email
|
||||
}
|
||||
pub fn into_parts(self) -> ContactParts {
|
||||
ContactParts {
|
||||
id: self.id,
|
||||
@@ -355,7 +513,13 @@ impl ContactGroup {
|
||||
/// Creates a new ContactGroup with generated id and timestamps
|
||||
pub fn new(address_book_id: Uuid, name: String) -> Self {
|
||||
let now = Utc::now();
|
||||
Self { id: Uuid::new_v4(), address_book_id, name, created_at: now, updated_at: now }
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
address_book_id,
|
||||
name,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstructs from persistence
|
||||
@@ -366,19 +530,40 @@ impl ContactGroup {
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
) -> Self {
|
||||
Self { id, address_book_id, name, created_at, updated_at }
|
||||
Self {
|
||||
id,
|
||||
address_book_id,
|
||||
name,
|
||||
created_at,
|
||||
updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Getters ---
|
||||
pub fn id(&self) -> &Uuid { &self.id }
|
||||
pub fn address_book_id(&self) -> &Uuid { &self.address_book_id }
|
||||
pub fn name(&self) -> &str { &self.name }
|
||||
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
|
||||
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
|
||||
pub fn id(&self) -> &Uuid {
|
||||
&self.id
|
||||
}
|
||||
pub fn address_book_id(&self) -> &Uuid {
|
||||
&self.address_book_id
|
||||
}
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
pub fn created_at(&self) -> &DateTime<Utc> {
|
||||
&self.created_at
|
||||
}
|
||||
pub fn updated_at(&self) -> &DateTime<Utc> {
|
||||
&self.updated_at
|
||||
}
|
||||
|
||||
// --- Setters ---
|
||||
pub fn set_name(&mut self, name: String) { self.name = name; self.updated_at = Utc::now(); }
|
||||
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
|
||||
pub fn set_name(&mut self, name: String) {
|
||||
self.name = name;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) {
|
||||
self.updated_at = updated_at;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ContactGroup {
|
||||
|
||||
@@ -179,8 +179,12 @@ impl Display for CalendarEventError {
|
||||
match self {
|
||||
CalendarEventError::InvalidSummary(msg) => write!(f, "Invalid event summary: {}", msg),
|
||||
CalendarEventError::InvalidDates(msg) => write!(f, "Invalid event dates: {}", msg),
|
||||
CalendarEventError::InvalidRecurrence(msg) => write!(f, "Invalid recurrence rule: {}", msg),
|
||||
CalendarEventError::InvalidICalData(msg) => write!(f, "Invalid iCalendar data: {}", msg),
|
||||
CalendarEventError::InvalidRecurrence(msg) => {
|
||||
write!(f, "Invalid recurrence rule: {}", msg)
|
||||
}
|
||||
CalendarEventError::InvalidICalData(msg) => {
|
||||
write!(f, "Invalid iCalendar data: {}", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +263,11 @@ impl File {
|
||||
}
|
||||
|
||||
/// 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
|
||||
let new_storage_path = match folder_path {
|
||||
Some(path) => path.join(&self.name),
|
||||
@@ -360,7 +364,8 @@ mod tests {
|
||||
100,
|
||||
"text/plain".to_string(),
|
||||
None,
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let renamed = file.with_name("newname.txt".to_string());
|
||||
assert!(renamed.is_ok());
|
||||
|
||||
@@ -194,7 +194,11 @@ impl Folder {
|
||||
}
|
||||
|
||||
/// 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
|
||||
let new_storage_path = match parent_path {
|
||||
Some(path) => path.join(&self.name),
|
||||
@@ -281,7 +285,8 @@ mod tests {
|
||||
"old_name".to_string(),
|
||||
storage_path,
|
||||
None,
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let renamed = folder.with_name("new_name".to_string());
|
||||
assert!(renamed.is_ok());
|
||||
|
||||
@@ -4,17 +4,13 @@ pub mod contact;
|
||||
pub mod entity_errors;
|
||||
pub mod file;
|
||||
pub mod folder;
|
||||
pub mod user;
|
||||
pub mod session;
|
||||
pub mod share;
|
||||
pub mod trashed_item;
|
||||
pub mod user;
|
||||
|
||||
// Re-exportar errores de entidades para facilitar el uso
|
||||
pub use entity_errors::{
|
||||
FileError, FileResult,
|
||||
FolderError, FolderResult,
|
||||
UserError, UserResult,
|
||||
ShareError, ShareResult,
|
||||
CalendarError, CalendarResult,
|
||||
CalendarEventError, CalendarEventResult,
|
||||
CalendarError, CalendarEventError, CalendarEventResult, CalendarResult, FileError, FileResult,
|
||||
FolderError, FolderResult, ShareError, ShareResult, UserError, UserResult,
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Session {
|
||||
|
||||
@@ -42,7 +42,9 @@ impl Share {
|
||||
) -> Result<Self, ShareError> {
|
||||
// Validate item_id
|
||||
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
|
||||
@@ -53,7 +55,9 @@ impl Share {
|
||||
.as_secs();
|
||||
|
||||
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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +242,10 @@ impl TryFrom<&str> for ShareItemType {
|
||||
match s.to_lowercase().as_str() {
|
||||
"file" => Ok(ShareItemType::File),
|
||||
"folder" => Ok(ShareItemType::Folder),
|
||||
_ => Err(ShareError::ValidationError(format!("Invalid item type: {}", s))),
|
||||
_ => Err(ShareError::ValidationError(format!(
|
||||
"Invalid item type: {}",
|
||||
s
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,9 +317,18 @@ mod tests {
|
||||
assert_eq!(ShareItemType::File.to_string(), "file");
|
||||
assert_eq!(ShareItemType::Folder.to_string(), "folder");
|
||||
|
||||
assert_eq!(ShareItemType::try_from("file").unwrap(), ShareItemType::File);
|
||||
assert_eq!(ShareItemType::try_from("folder").unwrap(), ShareItemType::Folder);
|
||||
assert_eq!(ShareItemType::try_from("FILE").unwrap(), ShareItemType::File);
|
||||
assert_eq!(
|
||||
ShareItemType::try_from("file").unwrap(),
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
// Re-export entity errors from the centralized module
|
||||
pub use super::entity_errors::{UserError, UserResult};
|
||||
@@ -59,7 +59,9 @@ impl User {
|
||||
) -> UserResult<Self> {
|
||||
// Validations
|
||||
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 {
|
||||
@@ -67,7 +69,9 @@ impl User {
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -104,9 +108,7 @@ impl User {
|
||||
));
|
||||
}
|
||||
if !email.contains('@') || email.len() < 5 {
|
||||
return Err(UserError::ValidationError(
|
||||
"Invalid email".to_string(),
|
||||
));
|
||||
return Err(UserError::ValidationError("Invalid email".to_string()));
|
||||
}
|
||||
let now = Utc::now();
|
||||
Ok(Self {
|
||||
|
||||
+15
-19
@@ -3,8 +3,8 @@
|
||||
//! This module contains domain-specific error types.
|
||||
//! 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::fmt::{Display, Formatter, Result as FmtResult};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Common Result type for the domain with DomainError as the standard error
|
||||
@@ -68,11 +68,7 @@ pub struct DomainError {
|
||||
|
||||
impl DomainError {
|
||||
/// Creates a new domain error
|
||||
pub fn new<S: Into<String>>(
|
||||
kind: ErrorKind,
|
||||
entity_type: &'static str,
|
||||
message: S,
|
||||
) -> Self {
|
||||
pub fn new<S: Into<String>>(kind: ErrorKind, entity_type: &'static str, message: S) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
entity_type,
|
||||
@@ -108,11 +104,7 @@ impl DomainError {
|
||||
|
||||
/// 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,
|
||||
)
|
||||
Self::new(ErrorKind::UnsupportedOperation, entity_type, message)
|
||||
}
|
||||
|
||||
/// Creates a timeout error
|
||||
@@ -212,7 +204,11 @@ pub trait ErrorContext<T, E> {
|
||||
C: Into<String>,
|
||||
F: FnOnce() -> C;
|
||||
|
||||
fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result<T, DomainError>;
|
||||
fn with_error_kind(
|
||||
self,
|
||||
kind: ErrorKind,
|
||||
entity_type: &'static str,
|
||||
) -> std::result::Result<T, DomainError>;
|
||||
}
|
||||
|
||||
impl<T, E: StdError + Send + Sync + 'static> ErrorContext<T, E> for std::result::Result<T, E> {
|
||||
@@ -221,26 +217,26 @@ impl<T, E: StdError + Send + Sync + 'static> ErrorContext<T, E> for std::result:
|
||||
C: Into<String>,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
DomainError {
|
||||
self.map_err(|e| DomainError {
|
||||
kind: ErrorKind::InternalError,
|
||||
entity_type: "Unknown",
|
||||
entity_id: None,
|
||||
message: context().into(),
|
||||
source: Some(Box::new(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn with_error_kind(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> {
|
||||
self.map_err(|e| DomainError {
|
||||
kind,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: format!("{}", e),
|
||||
source: Some(Box::new(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
use std::result::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::contact::AddressBook;
|
||||
@@ -9,14 +9,41 @@ pub type AddressBookRepositoryResult<T> = Result<T, DomainError>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AddressBookRepository: Send + Sync + 'static {
|
||||
async fn create_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook>;
|
||||
async fn update_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook>;
|
||||
async fn create_address_book(
|
||||
&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 get_address_book_by_id(&self, 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_address_book_by_id(
|
||||
&self,
|
||||
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 share_address_book(&self, 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)>>;
|
||||
async fn share_address_book(
|
||||
&self,
|
||||
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::domain::entities::calendar_event::CalendarEvent;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type CalendarEventRepositoryResult<T> = Result<T, DomainError>;
|
||||
|
||||
@@ -10,10 +10,16 @@ pub type CalendarEventRepositoryResult<T> = Result<T, DomainError>;
|
||||
#[async_trait]
|
||||
pub trait CalendarEventRepository: Send + Sync + 'static {
|
||||
/// 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
|
||||
async fn update_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent>;
|
||||
async fn update_event(
|
||||
&self,
|
||||
event: CalendarEvent,
|
||||
) -> CalendarEventRepositoryResult<CalendarEvent>;
|
||||
|
||||
/// Deletes a calendar event by ID
|
||||
async fn delete_event(&self, id: &Uuid) -> CalendarEventRepositoryResult<()>;
|
||||
@@ -22,34 +28,51 @@ pub trait CalendarEventRepository: Send + Sync + 'static {
|
||||
async fn find_event_by_id(&self, id: &Uuid) -> CalendarEventRepositoryResult<CalendarEvent>;
|
||||
|
||||
/// 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)
|
||||
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
|
||||
async fn get_events_in_time_range(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
start: &DateTime<Utc>,
|
||||
end: &DateTime<Utc>
|
||||
end: &DateTime<Utc>,
|
||||
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
|
||||
|
||||
/// 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
|
||||
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
|
||||
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
|
||||
async fn list_events_by_calendar_paginated(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
limit: i64,
|
||||
offset: i64
|
||||
offset: i64,
|
||||
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
|
||||
|
||||
/// Finds events with recurrence rules that might occur in a time range
|
||||
@@ -57,6 +80,6 @@ pub trait CalendarEventRepository: Send + Sync + 'static {
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
start: &DateTime<Utc>,
|
||||
end: &DateTime<Utc>
|
||||
end: &DateTime<Utc>,
|
||||
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::calendar::Calendar;
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type CalendarRepositoryResult<T> = Result<T, DomainError>;
|
||||
|
||||
@@ -21,38 +21,84 @@ pub trait CalendarRepository: Send + Sync + 'static {
|
||||
async fn find_calendar_by_id(&self, id: &Uuid) -> CalendarRepositoryResult<Calendar>;
|
||||
|
||||
/// 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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)
|
||||
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)>>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
use std::result::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
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 delete_contact(&self, id: &Uuid) -> ContactRepositoryResult<()>;
|
||||
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_contacts_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>>;
|
||||
async fn get_contact_by_uid(
|
||||
&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_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>>;
|
||||
async fn search_contacts(&self, address_book_id: &Uuid, query: &str) -> ContactRepositoryResult<Vec<Contact>>;
|
||||
async fn get_contacts_by_group(&self, group_id: &Uuid)
|
||||
-> ContactRepositoryResult<Vec<Contact>>;
|
||||
async fn search_contacts(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
query: &str,
|
||||
) -> ContactRepositoryResult<Vec<Contact>>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -26,9 +38,24 @@ pub trait ContactGroupRepository: Send + Sync + 'static {
|
||||
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup>;
|
||||
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()>;
|
||||
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 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>>;
|
||||
async fn get_groups_by_address_book(
|
||||
&self,
|
||||
address_book_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>>;
|
||||
}
|
||||
@@ -15,9 +15,9 @@ use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// FileReadRepository — read/query operations
|
||||
@@ -98,17 +98,14 @@ pub trait FileWriteRepository: Send + Sync + 'static {
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Renames a file (same folder, different name).
|
||||
async fn rename_file(
|
||||
&self,
|
||||
file_id: &str,
|
||||
new_name: &str,
|
||||
) -> Result<File, DomainError>;
|
||||
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<File, DomainError>;
|
||||
|
||||
/// Deletes a file.
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// 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).
|
||||
///
|
||||
@@ -128,7 +125,11 @@ pub trait FileWriteRepository: Send + Sync + 'static {
|
||||
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// 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)
|
||||
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::folder::Folder;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Domain port for folder persistence.
|
||||
///
|
||||
@@ -21,7 +21,11 @@ use crate::common::errors::DomainError;
|
||||
#[async_trait]
|
||||
pub trait FolderRepository: Send + Sync + 'static {
|
||||
/// 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
|
||||
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>;
|
||||
@@ -38,14 +42,18 @@ pub trait FolderRepository: Send + Sync + 'static {
|
||||
parent_id: Option<&str>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
include_total: bool
|
||||
include_total: bool,
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
|
||||
|
||||
/// Renames a folder
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
|
||||
|
||||
/// 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
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
|
||||
@@ -62,7 +70,11 @@ pub trait FolderRepository: Send + Sync + 'static {
|
||||
async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// 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)
|
||||
async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
pub mod address_book_repository;
|
||||
pub mod calendar_repository;
|
||||
pub mod calendar_event_repository;
|
||||
pub mod calendar_repository;
|
||||
pub mod contact_repository;
|
||||
pub mod file_repository;
|
||||
pub mod folder_repository;
|
||||
pub mod session_repository;
|
||||
pub mod settings_repository;
|
||||
pub mod share_repository;
|
||||
pub mod trash_repository;
|
||||
pub mod settings_repository;
|
||||
pub mod user_repository;
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::domain::entities::session::Session;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::session::Session;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SessionRepositoryError {
|
||||
@@ -20,15 +20,11 @@ pub type SessionRepositoryResult<T> = Result<T, SessionRepositoryError>;
|
||||
impl From<SessionRepositoryError> for DomainError {
|
||||
fn from(err: SessionRepositoryError) -> Self {
|
||||
match err {
|
||||
SessionRepositoryError::NotFound(msg) => {
|
||||
DomainError::not_found("Session", msg)
|
||||
},
|
||||
SessionRepositoryError::NotFound(msg) => DomainError::not_found("Session", msg),
|
||||
SessionRepositoryError::DatabaseError(msg) => {
|
||||
DomainError::internal_error("Database", msg)
|
||||
},
|
||||
SessionRepositoryError::Timeout(msg) => {
|
||||
DomainError::timeout("Database", msg)
|
||||
},
|
||||
}
|
||||
SessionRepositoryError::Timeout(msg) => DomainError::timeout("Database", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,10 +38,14 @@ pub trait SessionRepository: Send + Sync + 'static {
|
||||
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session>;
|
||||
|
||||
/// 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
|
||||
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
|
||||
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Repository for platform settings stored in the database.
|
||||
/// Settings are key-value pairs organized by category (e.g., "oidc", "general").
|
||||
@@ -10,7 +10,8 @@ pub trait SettingsRepository: Send + Sync + 'static {
|
||||
async fn get(&self, key: &str) -> Result<Option<String>, DomainError>;
|
||||
|
||||
/// 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(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -33,7 +32,11 @@ pub trait ShareRepository: Send + Sync + 'static {
|
||||
async fn find_by_token(&self, token: &str) -> Result<Share, ShareRepositoryError>;
|
||||
|
||||
/// 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
|
||||
async fn delete(&self, id: &str) -> Result<(), ShareRepositoryError>;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::entities::trashed_item::TrashedItem;
|
||||
use crate::common::errors::Result;
|
||||
use crate::domain::entities::trashed_item::TrashedItem;
|
||||
|
||||
#[async_trait]
|
||||
pub trait TrashRepository: Send + Sync {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::domain::entities::user::{User, UserRole};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::user::{User, UserRole};
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum UserRepositoryError {
|
||||
@@ -29,24 +29,14 @@ pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>;
|
||||
impl From<UserRepositoryError> for DomainError {
|
||||
fn from(err: UserRepositoryError) -> Self {
|
||||
match err {
|
||||
UserRepositoryError::NotFound(msg) => {
|
||||
DomainError::not_found("User", msg)
|
||||
},
|
||||
UserRepositoryError::AlreadyExists(msg) => {
|
||||
DomainError::already_exists("User", msg)
|
||||
},
|
||||
UserRepositoryError::DatabaseError(msg) => {
|
||||
DomainError::internal_error("Database", msg)
|
||||
},
|
||||
UserRepositoryError::ValidationError(msg) => {
|
||||
DomainError::validation_error(msg)
|
||||
},
|
||||
UserRepositoryError::Timeout(msg) => {
|
||||
DomainError::timeout("Database", msg)
|
||||
},
|
||||
UserRepositoryError::NotFound(msg) => DomainError::not_found("User", msg),
|
||||
UserRepositoryError::AlreadyExists(msg) => DomainError::already_exists("User", 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) => {
|
||||
DomainError::access_denied("User", msg)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +59,11 @@ pub trait UserRepository: Send + Sync + 'static {
|
||||
async fn update_user(&self, user: User) -> UserRepositoryResult<User>;
|
||||
|
||||
/// 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
|
||||
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()>;
|
||||
@@ -78,10 +72,12 @@ pub trait UserRepository: Send + Sync + 'static {
|
||||
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<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
|
||||
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
|
||||
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()>;
|
||||
@@ -93,10 +89,18 @@ pub trait UserRepository: Send + Sync + 'static {
|
||||
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>;
|
||||
|
||||
/// 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
|
||||
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
|
||||
async fn count_users(&self) -> UserRepositoryResult<i64>;
|
||||
|
||||
@@ -20,7 +20,9 @@ impl StoragePath {
|
||||
|
||||
/// Creates an empty path (root)
|
||||
pub fn root() -> Self {
|
||||
Self { segments: Vec::new() }
|
||||
Self {
|
||||
segments: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a path from a string with segments separated by /
|
||||
@@ -49,7 +51,9 @@ impl StoragePath {
|
||||
pub fn join(&self, segment: &str) -> Self {
|
||||
let mut new_segments = self.segments.clone();
|
||||
new_segments.push(segment.to_string());
|
||||
Self { segments: new_segments }
|
||||
Self {
|
||||
segments: new_segments,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the file name (last segment)
|
||||
@@ -63,7 +67,9 @@ impl StoragePath {
|
||||
None
|
||||
} else {
|
||||
let parent_segments = self.segments[..self.segments.len() - 1].to_vec();
|
||||
Some(Self { segments: parent_segments })
|
||||
Some(Self {
|
||||
segments: parent_segments,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,22 +4,22 @@
|
||||
//! the `CalendarRepository` and `CalendarEventRepository` domain repositories.
|
||||
//! It bridges the gap between the application layer and the infrastructure layer.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::calendar_dto::{
|
||||
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto,
|
||||
CreateEventDto, UpdateEventDto, CreateEventICalDto
|
||||
CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto,
|
||||
UpdateCalendarDto, UpdateEventDto,
|
||||
};
|
||||
use crate::application::ports::calendar_ports::CalendarStoragePort;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::domain::entities::calendar::Calendar;
|
||||
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_repository::CalendarRepository;
|
||||
|
||||
/// Adapter that implements CalendarStoragePort using domain repositories
|
||||
pub struct CalendarStorageAdapter {
|
||||
@@ -44,21 +44,29 @@ impl CalendarStorageAdapter {
|
||||
impl CalendarStoragePort for CalendarStorageAdapter {
|
||||
// Calendar operations
|
||||
|
||||
async fn create_calendar(&self, dto: CreateCalendarDto, owner_id: &str) -> Result<CalendarDto, DomainError> {
|
||||
let calendar = Calendar::new(
|
||||
dto.name,
|
||||
owner_id.to_string(),
|
||||
dto.description,
|
||||
dto.color,
|
||||
)?;
|
||||
async fn create_calendar(
|
||||
&self,
|
||||
dto: CreateCalendarDto,
|
||||
owner_id: &str,
|
||||
) -> Result<CalendarDto, DomainError> {
|
||||
let calendar = Calendar::new(dto.name, owner_id.to_string(), dto.description, dto.color)?;
|
||||
|
||||
let created = self.calendar_repository.create_calendar(calendar).await?;
|
||||
Ok(CalendarDto::from(created))
|
||||
}
|
||||
|
||||
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
async fn update_calendar(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
update: UpdateCalendarDto,
|
||||
) -> Result<CalendarDto, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Calendar",
|
||||
"Invalid calendar ID format",
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?;
|
||||
|
||||
@@ -77,100 +85,214 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
||||
}
|
||||
|
||||
async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Calendar",
|
||||
"Invalid calendar ID format",
|
||||
)
|
||||
})?;
|
||||
|
||||
// First delete all events in the calendar
|
||||
self.event_repository.delete_all_events_in_calendar(&uuid).await?;
|
||||
self.event_repository
|
||||
.delete_all_events_in_calendar(&uuid)
|
||||
.await?;
|
||||
|
||||
// Then delete the calendar itself
|
||||
self.calendar_repository.delete_calendar(&uuid).await
|
||||
}
|
||||
|
||||
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Calendar",
|
||||
"Invalid calendar ID format",
|
||||
)
|
||||
})?;
|
||||
|
||||
let calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?;
|
||||
Ok(CalendarDto::from(calendar))
|
||||
}
|
||||
|
||||
async fn list_calendars_by_owner(&self, owner_id: &str) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
let calendars = self.calendar_repository.list_calendars_by_owner(owner_id).await?;
|
||||
async fn list_calendars_by_owner(
|
||||
&self,
|
||||
owner_id: &str,
|
||||
) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
let calendars = self
|
||||
.calendar_repository
|
||||
.list_calendars_by_owner(owner_id)
|
||||
.await?;
|
||||
Ok(calendars.into_iter().map(CalendarDto::from).collect())
|
||||
}
|
||||
|
||||
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?;
|
||||
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 list_public_calendars(&self, limit: i64, offset: i64) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
let calendars = self.calendar_repository.list_public_calendars(limit, offset).await?;
|
||||
async fn list_public_calendars(
|
||||
&self,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
let calendars = self
|
||||
.calendar_repository
|
||||
.list_public_calendars(limit, offset)
|
||||
.await?;
|
||||
Ok(calendars.into_iter().map(CalendarDto::from).collect())
|
||||
}
|
||||
|
||||
async fn check_calendar_access(&self, calendar_id: &str, user_id: &str) -> Result<bool, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "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).map_err(|_| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Calendar",
|
||||
"Invalid calendar ID format",
|
||||
)
|
||||
})?;
|
||||
|
||||
self.calendar_repository.user_has_calendar_access(&uuid, user_id).await
|
||||
self.calendar_repository
|
||||
.user_has_calendar_access(&uuid, user_id)
|
||||
.await
|
||||
}
|
||||
|
||||
// Calendar sharing
|
||||
|
||||
async fn share_calendar(&self, calendar_id: &str, user_id: &str, access_level: &str) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
async fn share_calendar(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
user_id: &str,
|
||||
access_level: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Calendar",
|
||||
"Invalid calendar ID format",
|
||||
)
|
||||
})?;
|
||||
|
||||
self.calendar_repository.share_calendar(&uuid, user_id, access_level).await
|
||||
self.calendar_repository
|
||||
.share_calendar(&uuid, user_id, access_level)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove_calendar_sharing(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
async fn remove_calendar_sharing(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Calendar",
|
||||
"Invalid calendar ID format",
|
||||
)
|
||||
})?;
|
||||
|
||||
self.calendar_repository.remove_calendar_sharing(&uuid, user_id).await
|
||||
self.calendar_repository
|
||||
.remove_calendar_sharing(&uuid, user_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_calendar_shares(&self, calendar_id: &str) -> Result<Vec<(String, String)>, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
async fn get_calendar_shares(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
) -> Result<Vec<(String, String)>, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Calendar",
|
||||
"Invalid calendar ID format",
|
||||
)
|
||||
})?;
|
||||
|
||||
self.calendar_repository.get_calendar_shares(&uuid).await
|
||||
}
|
||||
|
||||
// Calendar properties
|
||||
|
||||
async fn set_calendar_property(&self, calendar_id: &str, property_name: &str, property_value: &str) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
async fn set_calendar_property(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
property_name: &str,
|
||||
property_value: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Calendar",
|
||||
"Invalid calendar ID format",
|
||||
)
|
||||
})?;
|
||||
|
||||
self.calendar_repository.set_calendar_property(&uuid, property_name, property_value).await
|
||||
self.calendar_repository
|
||||
.set_calendar_property(&uuid, property_name, property_value)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_calendar_property(&self, calendar_id: &str, property_name: &str) -> Result<Option<String>, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
async fn get_calendar_property(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
property_name: &str,
|
||||
) -> Result<Option<String>, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Calendar",
|
||||
"Invalid calendar ID format",
|
||||
)
|
||||
})?;
|
||||
|
||||
self.calendar_repository.get_calendar_property(&uuid, property_name).await
|
||||
self.calendar_repository
|
||||
.get_calendar_property(&uuid, property_name)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_calendar_properties(&self, calendar_id: &str) -> Result<HashMap<String, String>, DomainError> {
|
||||
let uuid = Uuid::parse_str(calendar_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
|
||||
async fn get_calendar_properties(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
) -> Result<HashMap<String, String>, 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).await
|
||||
self.calendar_repository
|
||||
.get_calendar_properties(&uuid)
|
||||
.await
|
||||
}
|
||||
|
||||
// Event operations
|
||||
|
||||
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"))?;
|
||||
let calendar_id = Uuid::parse_str(&dto.calendar_id).map_err(|_| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Event",
|
||||
"Invalid calendar ID format",
|
||||
)
|
||||
})?;
|
||||
|
||||
// Verify calendar exists and user has access
|
||||
let _calendar = self.calendar_repository.find_calendar_by_id(&calendar_id).await?;
|
||||
let _calendar = self
|
||||
.calendar_repository
|
||||
.find_calendar_by_id(&calendar_id)
|
||||
.await?;
|
||||
|
||||
// Generate basic iCal data
|
||||
let ical_data = format!(
|
||||
@@ -198,12 +320,23 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
||||
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"))?;
|
||||
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?;
|
||||
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())?;
|
||||
@@ -212,9 +345,14 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
||||
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"))?;
|
||||
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?;
|
||||
|
||||
@@ -248,33 +386,56 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
||||
}
|
||||
|
||||
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"))?;
|
||||
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 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"))?;
|
||||
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"))?;
|
||||
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?;
|
||||
let events = self
|
||||
.event_repository
|
||||
.list_events_by_calendar_paginated(&uuid, limit, offset)
|
||||
.await?;
|
||||
Ok(events.into_iter().map(CalendarEventDto::from).collect())
|
||||
}
|
||||
|
||||
@@ -282,12 +443,20 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
start: &DateTime<Utc>,
|
||||
end: &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 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?;
|
||||
let events = self
|
||||
.event_repository
|
||||
.get_events_in_time_range(&uuid, start, end)
|
||||
.await?;
|
||||
Ok(events.into_iter().map(CalendarEventDto::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,24 @@
|
||||
//! using the domain repositories. It bridges the gap between the application layer
|
||||
//! and the infrastructure layer for CardDAV functionality.
|
||||
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::address_book_dto::{
|
||||
AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto,
|
||||
ShareAddressBookDto, UnshareAddressBookDto
|
||||
AddressBookDto, CreateAddressBookDto, ShareAddressBookDto, UnshareAddressBookDto,
|
||||
UpdateAddressBookDto,
|
||||
};
|
||||
use crate::application::dtos::contact_dto::{
|
||||
ContactDto, CreateContactDto, UpdateContactDto, CreateContactVCardDto,
|
||||
ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto,
|
||||
EmailDto, PhoneDto, AddressDto
|
||||
AddressDto, ContactDto, ContactGroupDto, CreateContactDto, CreateContactGroupDto,
|
||||
CreateContactVCardDto, EmailDto, GroupMembershipDto, PhoneDto, UpdateContactDto,
|
||||
UpdateContactGroupDto,
|
||||
};
|
||||
use crate::application::ports::carddav_ports::{AddressBookUseCase, ContactUseCase};
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::domain::entities::contact::{AddressBook, Contact, ContactGroup, Email, Phone, Address};
|
||||
use crate::domain::entities::contact::{Address, AddressBook, Contact, ContactGroup, Email, Phone};
|
||||
use crate::domain::repositories::address_book_repository::AddressBookRepository;
|
||||
use crate::domain::repositories::contact_repository::{ContactRepository, ContactGroupRepository};
|
||||
use crate::domain::repositories::contact_repository::{ContactGroupRepository, ContactRepository};
|
||||
|
||||
/// Adapter that implements AddressBookUseCase and ContactUseCase using domain repositories
|
||||
pub struct ContactStorageAdapter {
|
||||
@@ -46,16 +46,28 @@ impl ContactStorageAdapter {
|
||||
|
||||
/// Helper to parse UUID from string
|
||||
fn parse_uuid(id: &str, entity_name: &'static str) -> Result<Uuid, DomainError> {
|
||||
Uuid::parse_str(id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, entity_name, format!("Invalid {} ID format", entity_name)))
|
||||
Uuid::parse_str(id).map_err(|_| {
|
||||
DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
entity_name,
|
||||
format!("Invalid {} ID format", entity_name),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper to check if user has access to an address book
|
||||
async fn check_address_book_access(&self, address_book_id: &Uuid, user_id: &str) -> Result<AddressBook, DomainError> {
|
||||
let address_book = self.address_book_repository
|
||||
async fn check_address_book_access(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
user_id: &str,
|
||||
) -> Result<AddressBook, DomainError> {
|
||||
let address_book = self
|
||||
.address_book_repository
|
||||
.get_address_book_by_id(address_book_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found")
|
||||
})?;
|
||||
|
||||
// Check if user is owner
|
||||
if address_book.owner_id() == user_id {
|
||||
@@ -68,20 +80,34 @@ impl ContactStorageAdapter {
|
||||
}
|
||||
|
||||
// Check if address book is shared with user
|
||||
let shares = self.address_book_repository.get_address_book_shares(address_book_id).await?;
|
||||
let shares = self
|
||||
.address_book_repository
|
||||
.get_address_book_shares(address_book_id)
|
||||
.await?;
|
||||
if shares.iter().any(|(shared_user, _)| shared_user == user_id) {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Access denied to address book"))
|
||||
Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"AddressBook",
|
||||
"Access denied to address book",
|
||||
))
|
||||
}
|
||||
|
||||
/// Helper to check write access
|
||||
async fn check_write_access(&self, address_book_id: &Uuid, user_id: &str) -> Result<AddressBook, DomainError> {
|
||||
let address_book = self.address_book_repository
|
||||
async fn check_write_access(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
user_id: &str,
|
||||
) -> Result<AddressBook, DomainError> {
|
||||
let address_book = self
|
||||
.address_book_repository
|
||||
.get_address_book_by_id(address_book_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found")
|
||||
})?;
|
||||
|
||||
// Owner always has write access
|
||||
if address_book.owner_id() == user_id {
|
||||
@@ -89,12 +115,22 @@ impl ContactStorageAdapter {
|
||||
}
|
||||
|
||||
// Check shares for write permission
|
||||
let shares = self.address_book_repository.get_address_book_shares(address_book_id).await?;
|
||||
if shares.iter().any(|(shared_user, can_write)| shared_user == user_id && *can_write) {
|
||||
let shares = self
|
||||
.address_book_repository
|
||||
.get_address_book_shares(address_book_id)
|
||||
.await?;
|
||||
if shares
|
||||
.iter()
|
||||
.any(|(shared_user, can_write)| shared_user == user_id && *can_write)
|
||||
{
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Write access denied"))
|
||||
Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"AddressBook",
|
||||
"Write access denied",
|
||||
))
|
||||
}
|
||||
|
||||
/// Convert EmailDto to domain Email
|
||||
@@ -147,11 +183,19 @@ impl ContactStorageAdapter {
|
||||
}
|
||||
|
||||
for email in contact.email() {
|
||||
vcard.push_str(&format!("EMAIL;TYPE={}:{}\n", email.r#type.to_uppercase(), email.email));
|
||||
vcard.push_str(&format!(
|
||||
"EMAIL;TYPE={}:{}\n",
|
||||
email.r#type.to_uppercase(),
|
||||
email.email
|
||||
));
|
||||
}
|
||||
|
||||
for phone in contact.phone() {
|
||||
vcard.push_str(&format!("TEL;TYPE={}:{}\n", phone.r#type.to_uppercase(), phone.number));
|
||||
vcard.push_str(&format!(
|
||||
"TEL;TYPE={}:{}\n",
|
||||
phone.r#type.to_uppercase(),
|
||||
phone.number
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(org) = contact.organization() {
|
||||
@@ -175,7 +219,10 @@ impl ContactStorageAdapter {
|
||||
|
||||
#[async_trait]
|
||||
impl AddressBookUseCase for ContactStorageAdapter {
|
||||
async fn create_address_book(&self, dto: CreateAddressBookDto) -> Result<AddressBookDto, DomainError> {
|
||||
async fn create_address_book(
|
||||
&self,
|
||||
dto: CreateAddressBookDto,
|
||||
) -> Result<AddressBookDto, DomainError> {
|
||||
let address_book = AddressBook::new(
|
||||
dto.name,
|
||||
dto.owner_id,
|
||||
@@ -184,11 +231,18 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
||||
dto.is_public.unwrap_or(false),
|
||||
);
|
||||
|
||||
let created = self.address_book_repository.create_address_book(address_book).await?;
|
||||
let created = self
|
||||
.address_book_repository
|
||||
.create_address_book(address_book)
|
||||
.await?;
|
||||
Ok(AddressBookDto::from(created))
|
||||
}
|
||||
|
||||
async fn update_address_book(&self, address_book_id: &str, update: UpdateAddressBookDto) -> Result<AddressBookDto, DomainError> {
|
||||
async fn update_address_book(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
update: UpdateAddressBookDto,
|
||||
) -> Result<AddressBookDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Check write access
|
||||
@@ -208,35 +262,64 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
||||
}
|
||||
address_book.set_updated_at(chrono::Utc::now());
|
||||
|
||||
let updated = self.address_book_repository.update_address_book(address_book).await?;
|
||||
let updated = self
|
||||
.address_book_repository
|
||||
.update_address_book(address_book)
|
||||
.await?;
|
||||
Ok(AddressBookDto::from(updated))
|
||||
}
|
||||
|
||||
async fn delete_address_book(&self, address_book_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_address_book(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Only owner can delete
|
||||
let address_book = self.address_book_repository
|
||||
let address_book = self
|
||||
.address_book_repository
|
||||
.get_address_book_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found")
|
||||
})?;
|
||||
|
||||
if address_book.owner_id() != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can delete address book"));
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"AddressBook",
|
||||
"Only owner can delete address book",
|
||||
));
|
||||
}
|
||||
|
||||
self.address_book_repository.delete_address_book(&uuid).await
|
||||
self.address_book_repository
|
||||
.delete_address_book(&uuid)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_address_book(&self, address_book_id: &str, user_id: &str) -> Result<AddressBookDto, DomainError> {
|
||||
async fn get_address_book(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<AddressBookDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
let address_book = self.check_address_book_access(&uuid, user_id).await?;
|
||||
Ok(AddressBookDto::from(address_book))
|
||||
}
|
||||
|
||||
async fn list_user_address_books(&self, user_id: &str) -> Result<Vec<AddressBookDto>, DomainError> {
|
||||
let owned = self.address_book_repository.get_address_books_by_owner(user_id).await?;
|
||||
let shared = self.address_book_repository.get_shared_address_books(user_id).await?;
|
||||
async fn list_user_address_books(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Vec<AddressBookDto>, DomainError> {
|
||||
let owned = self
|
||||
.address_book_repository
|
||||
.get_address_books_by_owner(user_id)
|
||||
.await?;
|
||||
let shared = self
|
||||
.address_book_repository
|
||||
.get_shared_address_books(user_id)
|
||||
.await?;
|
||||
|
||||
let mut all_books: Vec<AddressBook> = owned;
|
||||
all_books.extend(shared);
|
||||
@@ -245,56 +328,98 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
||||
}
|
||||
|
||||
async fn list_public_address_books(&self) -> Result<Vec<AddressBookDto>, DomainError> {
|
||||
let public = self.address_book_repository.get_public_address_books().await?;
|
||||
let public = self
|
||||
.address_book_repository
|
||||
.get_public_address_books()
|
||||
.await?;
|
||||
Ok(public.into_iter().map(AddressBookDto::from).collect())
|
||||
}
|
||||
|
||||
async fn share_address_book(&self, dto: ShareAddressBookDto, user_id: &str) -> Result<(), DomainError> {
|
||||
async fn share_address_book(
|
||||
&self,
|
||||
dto: ShareAddressBookDto,
|
||||
user_id: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||
|
||||
// Only owner can share
|
||||
let address_book = self.address_book_repository
|
||||
let address_book = self
|
||||
.address_book_repository
|
||||
.get_address_book_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found")
|
||||
})?;
|
||||
|
||||
if address_book.owner_id() != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can share"));
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"AddressBook",
|
||||
"Only owner can share",
|
||||
));
|
||||
}
|
||||
|
||||
self.address_book_repository.share_address_book(&uuid, &dto.user_id, dto.can_write).await
|
||||
self.address_book_repository
|
||||
.share_address_book(&uuid, &dto.user_id, dto.can_write)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn unshare_address_book(&self, dto: UnshareAddressBookDto, user_id: &str) -> Result<(), DomainError> {
|
||||
async fn unshare_address_book(
|
||||
&self,
|
||||
dto: UnshareAddressBookDto,
|
||||
user_id: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||
|
||||
// Only owner can unshare
|
||||
let address_book = self.address_book_repository
|
||||
let address_book = self
|
||||
.address_book_repository
|
||||
.get_address_book_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found")
|
||||
})?;
|
||||
|
||||
if address_book.owner_id() != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can unshare"));
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"AddressBook",
|
||||
"Only owner can unshare",
|
||||
));
|
||||
}
|
||||
|
||||
self.address_book_repository.unshare_address_book(&uuid, &dto.user_id).await
|
||||
self.address_book_repository
|
||||
.unshare_address_book(&uuid, &dto.user_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_address_book_shares(&self, address_book_id: &str, user_id: &str) -> Result<Vec<(String, bool)>, DomainError> {
|
||||
async fn get_address_book_shares(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<Vec<(String, bool)>, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Only owner can view shares
|
||||
let address_book = self.address_book_repository
|
||||
let address_book = self
|
||||
.address_book_repository
|
||||
.get_address_book_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found")
|
||||
})?;
|
||||
|
||||
if address_book.owner_id() != user_id {
|
||||
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can view shares"));
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"AddressBook",
|
||||
"Only owner can view shares",
|
||||
));
|
||||
}
|
||||
|
||||
self.address_book_repository.get_address_book_shares(&uuid).await
|
||||
self.address_book_repository
|
||||
.get_address_book_shares(&uuid)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +429,8 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(&address_book_id, &dto.user_id).await?;
|
||||
self.check_write_access(&address_book_id, &dto.user_id)
|
||||
.await?;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let mut contact = Contact::from_raw(
|
||||
@@ -338,11 +464,15 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
Ok(ContactDto::from(created))
|
||||
}
|
||||
|
||||
async fn create_contact_from_vcard(&self, dto: CreateContactVCardDto) -> Result<ContactDto, DomainError> {
|
||||
async fn create_contact_from_vcard(
|
||||
&self,
|
||||
dto: CreateContactVCardDto,
|
||||
) -> Result<ContactDto, DomainError> {
|
||||
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(&address_book_id, &dto.user_id).await?;
|
||||
self.check_write_access(&address_book_id, &dto.user_id)
|
||||
.await?;
|
||||
|
||||
// Parse vCard fields
|
||||
let now = chrono::Utc::now();
|
||||
@@ -381,10 +511,15 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
notes = Some(trimmed[5..].trim().to_string());
|
||||
} else if trimmed.starts_with("EMAIL") {
|
||||
if let Some(value) = trimmed.split(':').nth(1)
|
||||
&& !value.is_empty() {
|
||||
let email_type = if trimmed.contains("TYPE=HOME") { "home" }
|
||||
else if trimmed.contains("TYPE=WORK") { "work" }
|
||||
else { "other" };
|
||||
&& !value.is_empty()
|
||||
{
|
||||
let email_type = if trimmed.contains("TYPE=HOME") {
|
||||
"home"
|
||||
} else if trimmed.contains("TYPE=WORK") {
|
||||
"work"
|
||||
} else {
|
||||
"other"
|
||||
};
|
||||
emails.push(Email {
|
||||
email: value.trim().to_string(),
|
||||
r#type: email_type.to_string(),
|
||||
@@ -393,11 +528,18 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
}
|
||||
} else if trimmed.starts_with("TEL")
|
||||
&& let Some(value) = trimmed.split(':').nth(1)
|
||||
&& !value.is_empty() {
|
||||
let phone_type = if trimmed.contains("TYPE=CELL") || trimmed.contains("TYPE=MOBILE") { "mobile" }
|
||||
else if trimmed.contains("TYPE=HOME") { "home" }
|
||||
else if trimmed.contains("TYPE=WORK") { "work" }
|
||||
else { "other" };
|
||||
&& !value.is_empty()
|
||||
{
|
||||
let phone_type = if trimmed.contains("TYPE=CELL") || trimmed.contains("TYPE=MOBILE")
|
||||
{
|
||||
"mobile"
|
||||
} else if trimmed.contains("TYPE=HOME") {
|
||||
"home"
|
||||
} else if trimmed.contains("TYPE=WORK") {
|
||||
"work"
|
||||
} else {
|
||||
"other"
|
||||
};
|
||||
phones.push(Phone {
|
||||
number: value.trim().to_string(),
|
||||
r#type: phone_type.to_string(),
|
||||
@@ -435,16 +577,22 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
Ok(ContactDto::from(created))
|
||||
}
|
||||
|
||||
async fn update_contact(&self, contact_id: &str, update: UpdateContactDto) -> Result<ContactDto, DomainError> {
|
||||
async fn update_contact(
|
||||
&self,
|
||||
contact_id: &str,
|
||||
update: UpdateContactDto,
|
||||
) -> Result<ContactDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||
|
||||
let mut contact = self.contact_repository
|
||||
let mut contact = self
|
||||
.contact_repository
|
||||
.get_contact_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
||||
|
||||
// Check write access to the address book
|
||||
self.check_write_access(contact.address_book_id(), &update.user_id).await?;
|
||||
self.check_write_access(contact.address_book_id(), &update.user_id)
|
||||
.await?;
|
||||
|
||||
if let Some(full_name) = update.full_name {
|
||||
contact.set_full_name(Some(full_name));
|
||||
@@ -498,76 +646,108 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||
|
||||
let contact = self.contact_repository
|
||||
let contact = self
|
||||
.contact_repository
|
||||
.get_contact_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(contact.address_book_id(), user_id).await?;
|
||||
self.check_write_access(contact.address_book_id(), user_id)
|
||||
.await?;
|
||||
|
||||
self.contact_repository.delete_contact(&uuid).await
|
||||
}
|
||||
|
||||
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,
|
||||
) -> Result<ContactDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||
|
||||
let contact = self.contact_repository
|
||||
let contact = self
|
||||
.contact_repository
|
||||
.get_contact_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(contact.address_book_id(), user_id).await?;
|
||||
self.check_address_book_access(contact.address_book_id(), user_id)
|
||||
.await?;
|
||||
|
||||
Ok(ContactDto::from(contact))
|
||||
}
|
||||
|
||||
async fn list_contacts(&self, address_book_id: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError> {
|
||||
async fn list_contacts(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<Vec<ContactDto>, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&uuid, user_id).await?;
|
||||
|
||||
let contacts = self.contact_repository.get_contacts_by_address_book(&uuid).await?;
|
||||
let contacts = self
|
||||
.contact_repository
|
||||
.get_contacts_by_address_book(&uuid)
|
||||
.await?;
|
||||
Ok(contacts.into_iter().map(ContactDto::from).collect())
|
||||
}
|
||||
|
||||
async fn search_contacts(&self, address_book_id: &str, query: &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> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&uuid, user_id).await?;
|
||||
|
||||
let contacts = self.contact_repository.search_contacts(&uuid, query).await?;
|
||||
let contacts = self
|
||||
.contact_repository
|
||||
.search_contacts(&uuid, query)
|
||||
.await?;
|
||||
Ok(contacts.into_iter().map(ContactDto::from).collect())
|
||||
}
|
||||
|
||||
async fn create_group(&self, dto: CreateContactGroupDto) -> Result<ContactGroupDto, DomainError> {
|
||||
async fn create_group(
|
||||
&self,
|
||||
dto: CreateContactGroupDto,
|
||||
) -> Result<ContactGroupDto, DomainError> {
|
||||
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(&address_book_id, &dto.user_id).await?;
|
||||
self.check_write_access(&address_book_id, &dto.user_id)
|
||||
.await?;
|
||||
|
||||
let group = ContactGroup::new(
|
||||
address_book_id,
|
||||
dto.name,
|
||||
);
|
||||
let group = ContactGroup::new(address_book_id, dto.name);
|
||||
|
||||
let created = self.group_repository.create_group(group).await?;
|
||||
Ok(ContactGroupDto::from(created))
|
||||
}
|
||||
|
||||
async fn update_group(&self, group_id: &str, update: UpdateContactGroupDto) -> Result<ContactGroupDto, DomainError> {
|
||||
async fn update_group(
|
||||
&self,
|
||||
group_id: &str,
|
||||
update: UpdateContactGroupDto,
|
||||
) -> Result<ContactGroupDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
||||
|
||||
let mut group = self.group_repository
|
||||
let mut group = self
|
||||
.group_repository
|
||||
.get_group_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found")
|
||||
})?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(group.address_book_id(), &update.user_id).await?;
|
||||
self.check_write_access(group.address_book_id(), &update.user_id)
|
||||
.await?;
|
||||
|
||||
group.set_name(update.name);
|
||||
group.set_updated_at(chrono::Utc::now());
|
||||
@@ -579,122 +759,188 @@ impl ContactUseCase for ContactStorageAdapter {
|
||||
async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
||||
|
||||
let group = self.group_repository
|
||||
let group = self
|
||||
.group_repository
|
||||
.get_group_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found")
|
||||
})?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(group.address_book_id(), user_id).await?;
|
||||
self.check_write_access(group.address_book_id(), user_id)
|
||||
.await?;
|
||||
|
||||
self.group_repository.delete_group(&uuid).await
|
||||
}
|
||||
|
||||
async fn get_group(&self, group_id: &str, user_id: &str) -> Result<ContactGroupDto, DomainError> {
|
||||
async fn get_group(
|
||||
&self,
|
||||
group_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<ContactGroupDto, DomainError> {
|
||||
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
||||
|
||||
let group = self.group_repository
|
||||
let group = self
|
||||
.group_repository
|
||||
.get_group_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found")
|
||||
})?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(group.address_book_id(), user_id).await?;
|
||||
self.check_address_book_access(group.address_book_id(), user_id)
|
||||
.await?;
|
||||
|
||||
Ok(ContactGroupDto::from(group))
|
||||
}
|
||||
|
||||
async fn list_groups(&self, address_book_id: &str, user_id: &str) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||
async fn list_groups(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&uuid, user_id).await?;
|
||||
|
||||
let groups = self.group_repository.get_groups_by_address_book(&uuid).await?;
|
||||
let groups = self
|
||||
.group_repository
|
||||
.get_groups_by_address_book(&uuid)
|
||||
.await?;
|
||||
Ok(groups.into_iter().map(ContactGroupDto::from).collect())
|
||||
}
|
||||
|
||||
async fn add_contact_to_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError> {
|
||||
async fn add_contact_to_group(
|
||||
&self,
|
||||
dto: GroupMembershipDto,
|
||||
user_id: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
|
||||
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
|
||||
|
||||
let group = self.group_repository
|
||||
let group = self
|
||||
.group_repository
|
||||
.get_group_by_id(&group_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found")
|
||||
})?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(group.address_book_id(), user_id).await?;
|
||||
self.check_write_access(group.address_book_id(), user_id)
|
||||
.await?;
|
||||
|
||||
self.group_repository.add_contact_to_group(&group_id, &contact_id).await
|
||||
self.group_repository
|
||||
.add_contact_to_group(&group_id, &contact_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove_contact_from_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError> {
|
||||
async fn remove_contact_from_group(
|
||||
&self,
|
||||
dto: GroupMembershipDto,
|
||||
user_id: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
|
||||
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
|
||||
|
||||
let group = self.group_repository
|
||||
let group = self
|
||||
.group_repository
|
||||
.get_group_by_id(&group_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found")
|
||||
})?;
|
||||
|
||||
// Check write access
|
||||
self.check_write_access(group.address_book_id(), user_id).await?;
|
||||
self.check_write_access(group.address_book_id(), user_id)
|
||||
.await?;
|
||||
|
||||
self.group_repository.remove_contact_from_group(&group_id, &contact_id).await
|
||||
self.group_repository
|
||||
.remove_contact_from_group(&group_id, &contact_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_contacts_in_group(&self, group_id: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError> {
|
||||
async fn list_contacts_in_group(
|
||||
&self,
|
||||
group_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<Vec<ContactDto>, DomainError> {
|
||||
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
||||
|
||||
let group = self.group_repository
|
||||
let group = self
|
||||
.group_repository
|
||||
.get_group_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
|
||||
.ok_or_else(|| {
|
||||
DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found")
|
||||
})?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(group.address_book_id(), user_id).await?;
|
||||
self.check_address_book_access(group.address_book_id(), user_id)
|
||||
.await?;
|
||||
|
||||
let contacts = self.group_repository.get_contacts_in_group(&uuid).await?;
|
||||
Ok(contacts.into_iter().map(ContactDto::from).collect())
|
||||
}
|
||||
|
||||
async fn list_groups_for_contact(&self, contact_id: &str, user_id: &str) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||
async fn list_groups_for_contact(
|
||||
&self,
|
||||
contact_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||
|
||||
let contact = self.contact_repository
|
||||
let contact = self
|
||||
.contact_repository
|
||||
.get_contact_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(contact.address_book_id(), user_id).await?;
|
||||
self.check_address_book_access(contact.address_book_id(), user_id)
|
||||
.await?;
|
||||
|
||||
let groups = self.group_repository.get_groups_for_contact(&uuid).await?;
|
||||
Ok(groups.into_iter().map(ContactGroupDto::from).collect())
|
||||
}
|
||||
|
||||
async fn get_contact_vcard(&self, contact_id: &str, user_id: &str) -> Result<String, DomainError> {
|
||||
async fn get_contact_vcard(
|
||||
&self,
|
||||
contact_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<String, DomainError> {
|
||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||
|
||||
let contact = self.contact_repository
|
||||
let contact = self
|
||||
.contact_repository
|
||||
.get_contact_by_id(&uuid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(contact.address_book_id(), user_id).await?;
|
||||
self.check_address_book_access(contact.address_book_id(), user_id)
|
||||
.await?;
|
||||
|
||||
Ok(contact.vcard().to_string())
|
||||
}
|
||||
|
||||
async fn get_contacts_as_vcards(&self, address_book_id: &str, user_id: &str) -> Result<Vec<(String, String)>, DomainError> {
|
||||
async fn get_contacts_as_vcards(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<Vec<(String, String)>, DomainError> {
|
||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||
|
||||
// Check read access
|
||||
self.check_address_book_access(&uuid, user_id).await?;
|
||||
|
||||
let contacts = self.contact_repository.get_contacts_by_address_book(&uuid).await?;
|
||||
let contacts = self
|
||||
.contact_repository
|
||||
.get_contacts_by_address_book(&uuid)
|
||||
.await?;
|
||||
|
||||
Ok(contacts
|
||||
.into_iter()
|
||||
|
||||
@@ -59,7 +59,8 @@ impl IntoDomainError for std::io::Error {
|
||||
ErrorKind::InternalError,
|
||||
entity_type,
|
||||
format!("IO error: {}", self),
|
||||
).with_source(self)
|
||||
)
|
||||
.with_source(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,16 +70,15 @@ impl IntoDomainError for serde_json::Error {
|
||||
ErrorKind::InternalError,
|
||||
entity_type,
|
||||
format!("Serialization error: {}", self),
|
||||
).with_source(self)
|
||||
)
|
||||
.with_source(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDomainError for sqlx::Error {
|
||||
fn into_domain_error(self, entity_type: &'static str) -> DomainError {
|
||||
match &self {
|
||||
sqlx::Error::RowNotFound => {
|
||||
DomainError::not_found(entity_type, "Record not found")
|
||||
}
|
||||
sqlx::Error::RowNotFound => DomainError::not_found(entity_type, "Record not found"),
|
||||
sqlx::Error::Database(db_err) => {
|
||||
// Handle specific PostgreSQL error codes
|
||||
if db_err.code().is_some_and(|c| c == "23505") {
|
||||
@@ -88,14 +88,16 @@ impl IntoDomainError for sqlx::Error {
|
||||
ErrorKind::DatabaseError,
|
||||
entity_type,
|
||||
format!("Database error: {}", db_err),
|
||||
).with_source(self)
|
||||
)
|
||||
.with_source(self)
|
||||
}
|
||||
}
|
||||
_ => DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
entity_type,
|
||||
format!("Database error: {}", self),
|
||||
).with_source(self)
|
||||
)
|
||||
.with_source(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +118,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_serde_json_error_conversion() {
|
||||
let json_str = "{ invalid json }";
|
||||
let serde_error: serde_json::Error = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
|
||||
let serde_error: serde_json::Error =
|
||||
serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
|
||||
let domain_error = serde_error.into_domain_error("Config");
|
||||
|
||||
assert_eq!(domain_error.entity_type, "Config");
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::application::ports::auth_ports::TokenServicePort;
|
||||
use crate::application::services::auth_application_service::AuthApplicationService;
|
||||
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::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(
|
||||
config: &AppConfig,
|
||||
pool: Arc<PgPool>,
|
||||
folder_service: Option<Arc<FolderService>>
|
||||
folder_service: Option<Arc<FolderService>>,
|
||||
) -> Result<AuthServices> {
|
||||
// Create JWT token service (TokenServicePort implementation)
|
||||
let token_service: Arc<dyn TokenServicePort> = Arc::new(JwtTokenService::new(
|
||||
@@ -47,8 +47,11 @@ pub async fn create_auth_services(
|
||||
|
||||
// Configure OIDC service if enabled
|
||||
if config.oidc.enabled {
|
||||
tracing::info!("Initializing OIDC service (provider: {}, issuer: {})",
|
||||
config.oidc.provider_name, config.oidc.issuer_url);
|
||||
tracing::info!(
|
||||
"Initializing OIDC service (provider: {}, issuer: {})",
|
||||
config.oidc.provider_name,
|
||||
config.oidc.issuer_url
|
||||
);
|
||||
|
||||
let oidc_service = Arc::new(OidcService::new(config.oidc.clone()));
|
||||
auth_app_service = auth_app_service.with_oidc(oidc_service, config.oidc.clone());
|
||||
|
||||
+55
-18
@@ -1,18 +1,27 @@
|
||||
use sqlx::{postgres::PgPoolOptions, PgPool, Row};
|
||||
use anyhow::Result;
|
||||
use std::time::Duration;
|
||||
use crate::common::config::AppConfig;
|
||||
use anyhow::Result;
|
||||
use sqlx::{PgPool, Row, postgres::PgPoolOptions};
|
||||
use std::time::Duration;
|
||||
|
||||
pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
||||
tracing::info!("Initializing PostgreSQL connection with URL: {}",
|
||||
config.database.connection_string.replace("postgres://", "postgres://[user]:[pass]@"));
|
||||
tracing::info!(
|
||||
"Initializing PostgreSQL connection with URL: {}",
|
||||
config
|
||||
.database
|
||||
.connection_string
|
||||
.replace("postgres://", "postgres://[user]:[pass]@")
|
||||
);
|
||||
|
||||
let mut attempt = 0;
|
||||
const MAX_ATTEMPTS: usize = 5;
|
||||
|
||||
while attempt < MAX_ATTEMPTS {
|
||||
attempt += 1;
|
||||
tracing::info!("PostgreSQL connection attempt #{}/{}", attempt, MAX_ATTEMPTS);
|
||||
tracing::info!(
|
||||
"PostgreSQL connection attempt #{}/{}",
|
||||
attempt,
|
||||
MAX_ATTEMPTS
|
||||
);
|
||||
|
||||
match PgPoolOptions::new()
|
||||
.max_connections(config.database.max_connections)
|
||||
@@ -21,7 +30,8 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
||||
.idle_timeout(Duration::from_secs(config.database.idle_timeout_secs))
|
||||
.max_lifetime(Duration::from_secs(config.database.max_lifetime_secs))
|
||||
.connect(&config.database.connection_string)
|
||||
.await {
|
||||
.await
|
||||
{
|
||||
Ok(pool) => {
|
||||
match sqlx::query("SELECT 1").execute(&pool).await {
|
||||
Ok(_) => {
|
||||
@@ -32,7 +42,8 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
||||
if let Err(e) = apply_schema(&pool).await {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Database schema could not be applied: {}. \
|
||||
Run manually: psql -f db/schema.sql", e
|
||||
Run manually: psql -f db/schema.sql",
|
||||
e
|
||||
));
|
||||
}
|
||||
|
||||
@@ -47,17 +58,25 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
||||
}
|
||||
|
||||
return Ok(pool);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error verifying connection: {}", e);
|
||||
if attempt >= MAX_ATTEMPTS {
|
||||
return Err(anyhow::anyhow!("Error verifying PostgreSQL connection: {}", e));
|
||||
return Err(anyhow::anyhow!(
|
||||
"Error verifying PostgreSQL connection: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Error connecting to PostgreSQL (attempt {}/{}): {}", attempt, MAX_ATTEMPTS, e);
|
||||
tracing::error!(
|
||||
"Error connecting to PostgreSQL (attempt {}/{}): {}",
|
||||
attempt,
|
||||
MAX_ATTEMPTS,
|
||||
e
|
||||
);
|
||||
if attempt >= MAX_ATTEMPTS {
|
||||
return Err(anyhow::anyhow!("Error in PostgreSQL connection: {}", e));
|
||||
}
|
||||
@@ -66,14 +85,20 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("Could not establish PostgreSQL connection after {} attempts", MAX_ATTEMPTS))
|
||||
Err(anyhow::anyhow!(
|
||||
"Could not establish PostgreSQL connection after {} attempts",
|
||||
MAX_ATTEMPTS
|
||||
))
|
||||
}
|
||||
|
||||
/// Check whether the core auth tables exist in the database.
|
||||
async fn tables_exist(pool: &PgPool) -> bool {
|
||||
sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')")
|
||||
sqlx::query(
|
||||
"SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')",
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await.map(|row| row.get::<bool, _>(0))
|
||||
.await
|
||||
.map(|row| row.get::<bool, _>(0))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -87,7 +112,10 @@ async fn apply_schema(pool: &PgPool) -> Result<()> {
|
||||
match sqlx::raw_sql(schema_sql).execute(pool).await {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => {
|
||||
tracing::warn!("raw_sql failed ({}), falling back to statement-by-statement execution", e);
|
||||
tracing::warn!(
|
||||
"raw_sql failed ({}), falling back to statement-by-statement execution",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,8 +127,17 @@ async fn apply_schema(pool: &PgPool) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = sqlx::raw_sql(trimmed).execute(pool).await {
|
||||
let preview = if trimmed.len() > 200 { &trimmed[..200] } else { trimmed };
|
||||
tracing::error!("Schema statement {} failed: {}\n--- SQL ---\n{}\n-----------", i + 1, e, preview);
|
||||
let preview = if trimmed.len() > 200 {
|
||||
&trimmed[..200]
|
||||
} else {
|
||||
trimmed
|
||||
};
|
||||
tracing::error!(
|
||||
"Schema statement {} failed: {}\n--- SQL ---\n{}\n-----------",
|
||||
i + 1,
|
||||
e,
|
||||
preview
|
||||
);
|
||||
return Err(anyhow::anyhow!("Schema statement {} failed: {}", i + 1, e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,3 @@ pub mod auth_factory;
|
||||
pub mod db;
|
||||
pub mod repositories;
|
||||
pub mod services;
|
||||
|
||||
|
||||
@@ -86,7 +86,9 @@ impl FileWritePort for CompositeFileRepository {
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> Result<File, DomainError> {
|
||||
self.write.save_file(name, folder_id, content_type, content).await
|
||||
self.write
|
||||
.save_file(name, folder_id, content_type, content)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn save_file_from_stream(
|
||||
@@ -96,7 +98,9 @@ impl FileWritePort for CompositeFileRepository {
|
||||
content_type: String,
|
||||
stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
||||
) -> Result<File, DomainError> {
|
||||
self.write.save_file_from_stream(name, folder_id, content_type, stream).await
|
||||
self.write
|
||||
.save_file_from_stream(name, folder_id, content_type, stream)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn move_file(
|
||||
@@ -107,11 +111,7 @@ impl FileWritePort for CompositeFileRepository {
|
||||
self.write.move_file(file_id, target_folder_id).await
|
||||
}
|
||||
|
||||
async fn rename_file(
|
||||
&self,
|
||||
file_id: &str,
|
||||
new_name: &str,
|
||||
) -> Result<File, DomainError> {
|
||||
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<File, DomainError> {
|
||||
self.write.rename_file(file_id, new_name).await
|
||||
}
|
||||
|
||||
@@ -119,7 +119,11 @@ impl FileWritePort for CompositeFileRepository {
|
||||
self.write.delete_file(id).await
|
||||
}
|
||||
|
||||
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> {
|
||||
self.write.update_file_content(file_id, content).await
|
||||
}
|
||||
|
||||
@@ -130,14 +134,20 @@ impl FileWritePort for CompositeFileRepository {
|
||||
content_type: String,
|
||||
size: u64,
|
||||
) -> Result<(File, PathBuf), DomainError> {
|
||||
self.write.register_file_deferred(name, folder_id, content_type, size).await
|
||||
self.write
|
||||
.register_file_deferred(name, folder_id, content_type, size)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError> {
|
||||
self.write.move_to_trash(file_id).await
|
||||
}
|
||||
|
||||
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> {
|
||||
self.write.restore_from_trash(file_id, original_path).await
|
||||
}
|
||||
|
||||
|
||||
@@ -2,24 +2,26 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::{fs, time};
|
||||
use tokio::fs::File as TokioFile;
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
use futures::{Stream, StreamExt};
|
||||
use bytes::Bytes;
|
||||
use tokio::task;
|
||||
use futures::{Stream, StreamExt};
|
||||
use mime_guess::from_path;
|
||||
use tokio::fs::File as TokioFile;
|
||||
use tokio::task;
|
||||
use tokio::{fs, time};
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::application::ports::storage_ports::FileReadPort;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::infrastructure::repositories::repository_errors::{FileRepositoryResult, FileRepositoryError};
|
||||
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||
use crate::application::ports::cache_ports::MetadataCachePort;
|
||||
use crate::application::ports::storage_ports::FileReadPort;
|
||||
use crate::application::services::storage_mediator::StorageMediator;
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||
use crate::infrastructure::repositories::repository_errors::{
|
||||
FileRepositoryError, FileRepositoryResult,
|
||||
};
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
|
||||
/// Repository implementation for file **read** operations.
|
||||
///
|
||||
@@ -63,12 +65,13 @@ impl FileFsReadRepository {
|
||||
Self {
|
||||
root_path: PathBuf::from("./storage"),
|
||||
storage_mediator: Arc::new(
|
||||
crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub(),
|
||||
crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub(
|
||||
),
|
||||
),
|
||||
id_mapping_service: Arc::new(crate::common::stubs::StubIdMappingPort),
|
||||
path_service: Arc::new(PathService::new(PathBuf::from("./storage"))),
|
||||
metadata_cache: Arc::new(
|
||||
crate::infrastructure::services::file_metadata_cache::FileMetadataCache::default()
|
||||
crate::infrastructure::services::file_metadata_cache::FileMetadataCache::default(),
|
||||
) as Arc<dyn MetadataCachePort>,
|
||||
config: AppConfig::default(),
|
||||
parallel_processor: None,
|
||||
@@ -81,36 +84,61 @@ impl FileFsReadRepository {
|
||||
self.path_service.resolve_path(storage_path)
|
||||
}
|
||||
|
||||
async fn get_file_metadata_raw(&self, abs_path: &PathBuf) -> FileRepositoryResult<(u64, u64, u64)> {
|
||||
async fn get_file_metadata_raw(
|
||||
&self,
|
||||
abs_path: &PathBuf,
|
||||
) -> FileRepositoryResult<(u64, u64, u64)> {
|
||||
// Cache first
|
||||
if let Some(cached) = self.metadata_cache.get_metadata(abs_path).await
|
||||
&& let (Some(s), Some(c), Some(m)) = (cached.size, cached.created_at, cached.modified_at) {
|
||||
&& let (Some(s), Some(c), Some(m)) =
|
||||
(cached.size, cached.created_at, cached.modified_at)
|
||||
{
|
||||
return Ok((s, c, m));
|
||||
}
|
||||
let metadata = time::timeout(self.config.timeouts.file_timeout(), fs::metadata(abs_path))
|
||||
.await
|
||||
.map_err(|_| FileRepositoryError::StorageError(format!("Timeout metadata: {}", abs_path.display())))?
|
||||
.map_err(|_| {
|
||||
FileRepositoryError::StorageError(format!(
|
||||
"Timeout metadata: {}",
|
||||
abs_path.display()
|
||||
))
|
||||
})?
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
let size = metadata.len();
|
||||
let created_at = metadata.created()
|
||||
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
let created_at = metadata
|
||||
.created()
|
||||
.map(|t| {
|
||||
t.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let modified_at = metadata.modified()
|
||||
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
let modified_at = metadata
|
||||
.modified()
|
||||
.map(|t| {
|
||||
t.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let _ = self.metadata_cache.refresh_metadata(abs_path).await;
|
||||
Ok((size, created_at, modified_at))
|
||||
}
|
||||
|
||||
async fn get_file_by_id(&self, id: &str) -> FileRepositoryResult<File> {
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(id).await
|
||||
let storage_path = self
|
||||
.id_mapping_service
|
||||
.get_path_by_id(id)
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::Other(e.to_string()))?;
|
||||
let abs_path = self.resolve_storage_path(&storage_path);
|
||||
|
||||
if !abs_path.exists() || !abs_path.is_file() {
|
||||
return Err(FileRepositoryError::NotFound(
|
||||
format!("File {} not found at {}", id, storage_path.to_string()),
|
||||
));
|
||||
return Err(FileRepositoryError::NotFound(format!(
|
||||
"File {} not found at {}",
|
||||
id,
|
||||
storage_path.to_string()
|
||||
)));
|
||||
}
|
||||
|
||||
let (size, created_at, modified_at) = self.get_file_metadata_raw(&abs_path).await?;
|
||||
@@ -120,8 +148,14 @@ impl FileFsReadRepository {
|
||||
let mime_type = from_path(&abs_path).first_or_octet_stream().to_string();
|
||||
|
||||
File::with_timestamps(
|
||||
id.to_string(), name, storage_path, size, mime_type, None,
|
||||
created_at, modified_at,
|
||||
id.to_string(),
|
||||
name,
|
||||
storage_path,
|
||||
size,
|
||||
mime_type,
|
||||
None,
|
||||
created_at,
|
||||
modified_at,
|
||||
)
|
||||
.map_err(|e| FileRepositoryError::Other(e.to_string()))
|
||||
}
|
||||
@@ -153,18 +187,14 @@ impl FileReadPort for FileFsReadRepository {
|
||||
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError> {
|
||||
let folder_storage_path = match folder_id {
|
||||
Some(id) => {
|
||||
match self.storage_mediator.get_folder_path(id).await {
|
||||
Some(id) => match self.storage_mediator.get_folder_path(id).await {
|
||||
Ok(path) => {
|
||||
let lossy = path.to_string_lossy().to_string();
|
||||
let folder_name = path.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.unwrap_or(&lossy);
|
||||
let folder_name = path.file_name().and_then(|f| f.to_str()).unwrap_or(&lossy);
|
||||
StoragePath::from_string(folder_name)
|
||||
}
|
||||
Err(_) => return Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
},
|
||||
None => StoragePath::root(),
|
||||
};
|
||||
|
||||
@@ -174,16 +204,24 @@ impl FileReadPort for FileFsReadRepository {
|
||||
}
|
||||
|
||||
let mut files_result = Vec::new();
|
||||
let mut entries = fs::read_dir(&abs_folder_path).await
|
||||
let mut entries = fs::read_dir(&abs_folder_path)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
while let Some(entry) = entries.next_entry().await
|
||||
while let Some(entry) = entries
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?
|
||||
{
|
||||
let path = entry.path();
|
||||
if !path.is_file() { continue; }
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
if file_name.starts_with('.') || file_name == "folder_ids.json" || file_name == "file_ids.json" {
|
||||
if file_name.starts_with('.')
|
||||
|| file_name == "folder_ids.json"
|
||||
|| file_name == "file_ids.json"
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let metadata = match fs::metadata(&path).await {
|
||||
@@ -191,20 +229,43 @@ impl FileReadPort for FileFsReadRepository {
|
||||
Err(_) => continue,
|
||||
};
|
||||
let file_storage_path = folder_storage_path.join(&file_name);
|
||||
let id = match self.id_mapping_service.get_or_create_id(&file_storage_path).await {
|
||||
let id = match self
|
||||
.id_mapping_service
|
||||
.get_or_create_id(&file_storage_path)
|
||||
.await
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let size = metadata.len();
|
||||
let created_at = metadata.created()
|
||||
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
let created_at = metadata
|
||||
.created()
|
||||
.map(|t| {
|
||||
t.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let modified_at = metadata.modified()
|
||||
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
let modified_at = metadata
|
||||
.modified()
|
||||
.map(|t| {
|
||||
t.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let mime_type = from_path(&path).first_or_octet_stream().to_string();
|
||||
|
||||
match File::with_timestamps(id, file_name, file_storage_path, size, mime_type, folder_id.map(String::from), created_at, modified_at) {
|
||||
match File::with_timestamps(
|
||||
id,
|
||||
file_name,
|
||||
file_storage_path,
|
||||
size,
|
||||
mime_type,
|
||||
folder_id.map(String::from),
|
||||
created_at,
|
||||
modified_at,
|
||||
) {
|
||||
Ok(file) => files_result.push(file),
|
||||
Err(_) => continue,
|
||||
}
|
||||
@@ -216,23 +277,39 @@ impl FileReadPort for FileFsReadRepository {
|
||||
}
|
||||
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError> {
|
||||
let file = self.get_file_by_id(id).await
|
||||
let file = self
|
||||
.get_file_by_id(id)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let abs_path = self.resolve_storage_path(file.storage_path());
|
||||
|
||||
let metadata = time::timeout(self.config.timeouts.file_timeout(), fs::metadata(&abs_path))
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", format!("Timeout metadata: {}", abs_path.display())))?
|
||||
.map_err(|_| {
|
||||
DomainError::internal_error(
|
||||
"File",
|
||||
format!("Timeout metadata: {}", abs_path.display()),
|
||||
)
|
||||
})?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let file_size = metadata.len();
|
||||
|
||||
if !self.config.resources.can_load_in_memory(file_size) {
|
||||
return Err(DomainError::internal_error("File",
|
||||
format!("File too large for memory: {} MB", file_size / (1024 * 1024))));
|
||||
return Err(DomainError::internal_error(
|
||||
"File",
|
||||
format!(
|
||||
"File too large for memory: {} MB",
|
||||
file_size / (1024 * 1024)
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Parallel read for very large files
|
||||
if self.config.resources.needs_parallel_processing(file_size, &self.config.concurrency) {
|
||||
if self
|
||||
.config
|
||||
.resources
|
||||
.needs_parallel_processing(file_size, &self.config.concurrency)
|
||||
{
|
||||
let content = if let Some(processor) = &self.parallel_processor {
|
||||
processor.read_file_parallel(&abs_path).await
|
||||
} else {
|
||||
@@ -247,13 +324,14 @@ impl FileReadPort for FileFsReadRepository {
|
||||
let abs_clone = abs_path.clone();
|
||||
let chunk_size = self.config.resources.chunk_size_bytes;
|
||||
let content = task::spawn_blocking(move || -> std::io::Result<Vec<u8>> {
|
||||
use std::io::{Read, BufReader};
|
||||
use std::io::{BufReader, Read};
|
||||
let file = std::fs::File::open(&abs_clone)?;
|
||||
let mut reader = BufReader::with_capacity(chunk_size, file);
|
||||
let mut buf = Vec::with_capacity(file_size as usize);
|
||||
reader.read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
}).await
|
||||
})
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
return Ok(content);
|
||||
@@ -262,7 +340,12 @@ impl FileReadPort for FileFsReadRepository {
|
||||
// Small files — async read
|
||||
time::timeout(self.config.timeouts.file_timeout(), fs::read(&abs_path))
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", format!("Timeout reading: {}", abs_path.display())))?
|
||||
.map_err(|_| {
|
||||
DomainError::internal_error(
|
||||
"File",
|
||||
format!("Timeout reading: {}", abs_path.display()),
|
||||
)
|
||||
})?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))
|
||||
}
|
||||
|
||||
@@ -270,7 +353,9 @@ impl FileReadPort for FileFsReadRepository {
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
||||
let file = self.get_file_by_id(id).await
|
||||
let file = self
|
||||
.get_file_by_id(id)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let abs_path = self.resolve_storage_path(file.storage_path());
|
||||
|
||||
@@ -281,15 +366,22 @@ impl FileReadPort for FileFsReadRepository {
|
||||
let file_size = metadata.len();
|
||||
let is_large = self.config.resources.is_large_file(file_size);
|
||||
|
||||
let fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::open(&abs_path))
|
||||
let fh = time::timeout(
|
||||
self.config.timeouts.file_timeout(),
|
||||
TokioFile::open(&abs_path),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout opening file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
let chunk_size = if is_large { self.config.resources.chunk_size_bytes } else { 4096 };
|
||||
let chunk_size = if is_large {
|
||||
self.config.resources.chunk_size_bytes
|
||||
} else {
|
||||
4096
|
||||
};
|
||||
let codec = BytesCodec::new();
|
||||
let stream = FramedRead::with_capacity(fh, codec, chunk_size)
|
||||
.map(|r| r.map(|bm| bm.freeze()));
|
||||
let stream =
|
||||
FramedRead::with_capacity(fh, codec, chunk_size).map(|r| r.map(|bm| bm.freeze()));
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
|
||||
@@ -301,7 +393,9 @@ impl FileReadPort for FileFsReadRepository {
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
||||
use tokio::io::AsyncSeekExt;
|
||||
|
||||
let file = self.get_file_by_id(id).await
|
||||
let file = self
|
||||
.get_file_by_id(id)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let abs_path = self.resolve_storage_path(file.storage_path());
|
||||
|
||||
@@ -311,31 +405,43 @@ impl FileReadPort for FileFsReadRepository {
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let file_size = metadata.len();
|
||||
if start >= file_size {
|
||||
return Err(DomainError::internal_error("File",
|
||||
format!("Range start {} beyond file size {}", start, file_size)));
|
||||
return Err(DomainError::internal_error(
|
||||
"File",
|
||||
format!("Range start {} beyond file size {}", start, file_size),
|
||||
));
|
||||
}
|
||||
let actual_end = end.map(|e| e.min(file_size - 1)).unwrap_or(file_size - 1);
|
||||
let range_length = actual_end - start + 1;
|
||||
|
||||
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::open(&abs_path))
|
||||
let mut fh = time::timeout(
|
||||
self.config.timeouts.file_timeout(),
|
||||
TokioFile::open(&abs_path),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout opening file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.seek(std::io::SeekFrom::Start(start)).await
|
||||
fh.seek(std::io::SeekFrom::Start(start))
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
let chunk_size = if range_length > 1024 * 1024 { self.config.resources.chunk_size_bytes } else { 8192 };
|
||||
let chunk_size = if range_length > 1024 * 1024 {
|
||||
self.config.resources.chunk_size_bytes
|
||||
} else {
|
||||
8192
|
||||
};
|
||||
use tokio::io::AsyncReadExt;
|
||||
let limited = fh.take(range_length);
|
||||
let codec = BytesCodec::new();
|
||||
let stream = FramedRead::with_capacity(limited, codec, chunk_size)
|
||||
.map(|r| r.map(|bm| bm.freeze()));
|
||||
let stream =
|
||||
FramedRead::with_capacity(limited, codec, chunk_size).map(|r| r.map(|bm| bm.freeze()));
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
|
||||
async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError> {
|
||||
use memmap2::Mmap;
|
||||
let file = self.get_file_by_id(id).await
|
||||
let file = self
|
||||
.get_file_by_id(id)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let abs_path = self.resolve_storage_path(file.storage_path());
|
||||
let path_clone = abs_path.clone();
|
||||
@@ -346,7 +452,8 @@ impl FileReadPort for FileFsReadRepository {
|
||||
let mmap = unsafe { Mmap::map(&fh) }
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
Ok(Bytes::copy_from_slice(&mmap[..]))
|
||||
}).await
|
||||
})
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::{Stream, StreamExt};
|
||||
use mime_guess::from_path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use tokio::{fs, time};
|
||||
use tokio::fs::File as TokioFile;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use futures::{Stream, StreamExt};
|
||||
use bytes::Bytes;
|
||||
use mime_guess::from_path;
|
||||
use tokio::task;
|
||||
use tokio::{fs, time};
|
||||
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::application::ports::storage_ports::FileWritePort;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::infrastructure::repositories::repository_errors::{FileRepositoryResult, FileRepositoryError};
|
||||
use crate::infrastructure::services::file_system_utils::FileSystemUtils;
|
||||
use crate::application::ports::cache_ports::MetadataCachePort;
|
||||
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||
use crate::application::ports::storage_ports::FileWritePort;
|
||||
use crate::application::services::storage_mediator::StorageMediator;
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||
use crate::infrastructure::repositories::repository_errors::{
|
||||
FileRepositoryError, FileRepositoryResult,
|
||||
};
|
||||
use crate::infrastructure::services::file_system_utils::FileSystemUtils;
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
|
||||
/// Repository implementation for file **write** operations.
|
||||
///
|
||||
@@ -47,7 +49,15 @@ impl FileFsWriteRepository {
|
||||
config: AppConfig,
|
||||
parallel_processor: Option<Arc<ParallelFileProcessor>>,
|
||||
) -> Self {
|
||||
Self { root_path, storage_mediator, id_mapping_service, path_service, metadata_cache, config, parallel_processor }
|
||||
Self {
|
||||
root_path,
|
||||
storage_mediator,
|
||||
id_mapping_service,
|
||||
path_service,
|
||||
metadata_cache,
|
||||
config,
|
||||
parallel_processor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub for testing (does not perform real I/O).
|
||||
@@ -55,12 +65,13 @@ impl FileFsWriteRepository {
|
||||
Self {
|
||||
root_path: PathBuf::from("./storage"),
|
||||
storage_mediator: Arc::new(
|
||||
crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub(),
|
||||
crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub(
|
||||
),
|
||||
),
|
||||
id_mapping_service: Arc::new(crate::common::stubs::StubIdMappingPort),
|
||||
path_service: Arc::new(PathService::new(PathBuf::from("./storage"))),
|
||||
metadata_cache: Arc::new(
|
||||
crate::infrastructure::services::file_metadata_cache::FileMetadataCache::default()
|
||||
crate::infrastructure::services::file_metadata_cache::FileMetadataCache::default(),
|
||||
) as Arc<dyn MetadataCachePort>,
|
||||
config: AppConfig::default(),
|
||||
parallel_processor: None,
|
||||
@@ -78,14 +89,23 @@ impl FileFsWriteRepository {
|
||||
time::timeout(
|
||||
self.config.timeouts.dir_timeout(),
|
||||
FileSystemUtils::create_dir_with_sync(parent),
|
||||
).await
|
||||
.map_err(|_| FileRepositoryError::StorageError(format!("Timeout creating dir: {}", parent.display())))?
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
FileRepositoryError::StorageError(format!(
|
||||
"Timeout creating dir: {}",
|
||||
parent.display()
|
||||
))
|
||||
})?
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> FileRepositoryResult<bool> {
|
||||
async fn file_exists_at_storage_path(
|
||||
&self,
|
||||
storage_path: &StoragePath,
|
||||
) -> FileRepositoryResult<bool> {
|
||||
let abs = self.resolve_storage_path(storage_path);
|
||||
if let Some(is_file) = self.metadata_cache.is_file(&abs).await {
|
||||
return Ok(is_file);
|
||||
@@ -96,22 +116,46 @@ impl FileFsWriteRepository {
|
||||
Ok(m.is_file())
|
||||
}
|
||||
Ok(Err(_)) => Ok(false),
|
||||
Err(_) => Err(FileRepositoryError::StorageError(format!("Timeout: {}", abs.display()))),
|
||||
Err(_) => Err(FileRepositoryError::StorageError(format!(
|
||||
"Timeout: {}",
|
||||
abs.display()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_file_metadata_raw(&self, abs_path: &PathBuf) -> FileRepositoryResult<(u64, u64, u64)> {
|
||||
async fn get_file_metadata_raw(
|
||||
&self,
|
||||
abs_path: &PathBuf,
|
||||
) -> FileRepositoryResult<(u64, u64, u64)> {
|
||||
if let Some(cached) = self.metadata_cache.get_metadata(abs_path).await
|
||||
&& let (Some(s), Some(c), Some(m)) = (cached.size, cached.created_at, cached.modified_at) {
|
||||
&& let (Some(s), Some(c), Some(m)) =
|
||||
(cached.size, cached.created_at, cached.modified_at)
|
||||
{
|
||||
return Ok((s, c, m));
|
||||
}
|
||||
let meta = time::timeout(self.config.timeouts.file_timeout(), fs::metadata(abs_path))
|
||||
.await
|
||||
.map_err(|_| FileRepositoryError::StorageError(format!("Timeout: {}", abs_path.display())))?
|
||||
.map_err(|_| {
|
||||
FileRepositoryError::StorageError(format!("Timeout: {}", abs_path.display()))
|
||||
})?
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
let s = meta.len();
|
||||
let c = meta.created().map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()).unwrap_or(0);
|
||||
let m = meta.modified().map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()).unwrap_or(0);
|
||||
let c = meta
|
||||
.created()
|
||||
.map(|t| {
|
||||
t.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let m = meta
|
||||
.modified()
|
||||
.map(|t| {
|
||||
t.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let _ = self.metadata_cache.refresh_metadata(abs_path).await;
|
||||
Ok((s, c, m))
|
||||
}
|
||||
@@ -159,11 +203,16 @@ impl FileFsWriteRepository {
|
||||
Err(_) => 0,
|
||||
};
|
||||
if self.config.resources.is_large_file(file_size) {
|
||||
task::spawn_blocking(move || { let _ = std::fs::remove_file(&abs_path); })
|
||||
task::spawn_blocking(move || {
|
||||
let _ = std::fs::remove_file(&abs_path);
|
||||
})
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::Other(e.to_string()))?;
|
||||
} else {
|
||||
time::timeout(self.config.timeouts.file_timeout(), fs::remove_file(&abs_path))
|
||||
time::timeout(
|
||||
self.config.timeouts.file_timeout(),
|
||||
fs::remove_file(&abs_path),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| FileRepositoryError::StorageError("Timeout deleting file".into()))?
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
@@ -177,11 +226,14 @@ impl FileFsWriteRepository {
|
||||
match self.id_mapping_service.save_changes().await {
|
||||
Ok(_) => {
|
||||
if let Ok(verified) = self.id_mapping_service.get_path_by_id(id).await
|
||||
&& verified.to_string() == expected_path {
|
||||
&& verified.to_string() == expected_path
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if attempt == 3 {
|
||||
return Err(FileRepositoryError::Other("Failed to verify ID mapping after 3 attempts".into()));
|
||||
return Err(FileRepositoryError::Other(
|
||||
"Failed to verify ID mapping after 3 attempts".into(),
|
||||
));
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
@@ -189,7 +241,12 @@ impl FileFsWriteRepository {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
tracing::warn!("ID mapping save retry {}: {}", attempt, e);
|
||||
}
|
||||
Err(e) => return Err(FileRepositoryError::Other(format!("Save ID mapping failed: {}", e))),
|
||||
Err(e) => {
|
||||
return Err(FileRepositoryError::Other(format!(
|
||||
"Save ID mapping failed: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -229,49 +286,97 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
content: Vec<u8>,
|
||||
) -> Result<File, DomainError> {
|
||||
let folder_path = self.resolve_folder_path(&folder_id).await;
|
||||
let (file_storage_path, actual_name) = self.unique_file_path(&folder_path, &name).await.map_err(map_repo_err)?;
|
||||
let (file_storage_path, actual_name) = self
|
||||
.unique_file_path(&folder_path, &name)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
let abs_path = self.resolve_storage_path(&file_storage_path);
|
||||
self.ensure_parent_directory(&abs_path).await.map_err(map_repo_err)?;
|
||||
self.ensure_parent_directory(&abs_path)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
|
||||
let content_size = content.len() as u64;
|
||||
|
||||
// Write strategy based on file size
|
||||
if self.config.resources.needs_parallel_processing(content_size, &self.config.concurrency) {
|
||||
if self
|
||||
.config
|
||||
.resources
|
||||
.needs_parallel_processing(content_size, &self.config.concurrency)
|
||||
{
|
||||
if let Some(proc) = &self.parallel_processor {
|
||||
proc.write_file_parallel(&abs_path, &content).await.map_err(map_repo_err)?;
|
||||
proc.write_file_parallel(&abs_path, &content)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
} else {
|
||||
let proc = ParallelFileProcessor::new(self.config.clone());
|
||||
proc.write_file_parallel(&abs_path, &content).await.map_err(map_repo_err)?;
|
||||
proc.write_file_parallel(&abs_path, &content)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
}
|
||||
} else if content_size > self.config.resources.large_file_threshold_mb * 1024 * 1024 {
|
||||
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::create(&abs_path))
|
||||
let mut fh = time::timeout(
|
||||
self.config.timeouts.file_timeout(),
|
||||
TokioFile::create(&abs_path),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout creating file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let chunk_size = self.config.resources.chunk_size_bytes;
|
||||
for chunk in content.chunks(chunk_size) {
|
||||
fh.write_all(chunk).await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.write_all(chunk)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
}
|
||||
fh.flush().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.flush()
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
} else {
|
||||
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::create(&abs_path))
|
||||
let mut fh = time::timeout(
|
||||
self.config.timeouts.file_timeout(),
|
||||
TokioFile::create(&abs_path),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout creating file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.write_all(&content).await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.flush().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.write_all(&content)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.flush()
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
}
|
||||
|
||||
let (size, created_at, modified_at) = self.get_file_metadata_raw(&abs_path).await.map_err(map_repo_err)?;
|
||||
let mime = if content_type.is_empty() { from_path(&abs_path).first_or_octet_stream().to_string() } else { content_type };
|
||||
let id = self.id_mapping_service.get_or_create_id(&file_storage_path).await
|
||||
let (size, created_at, modified_at) = self
|
||||
.get_file_metadata_raw(&abs_path)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
let mime = if content_type.is_empty() {
|
||||
from_path(&abs_path).first_or_octet_stream().to_string()
|
||||
} else {
|
||||
content_type
|
||||
};
|
||||
let id = self
|
||||
.id_mapping_service
|
||||
.get_or_create_id(&file_storage_path)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let path_string = file_storage_path.to_string();
|
||||
|
||||
let file = File::with_timestamps(id.clone(), actual_name, file_storage_path, size, mime, folder_id, created_at, modified_at)
|
||||
let file = File::with_timestamps(
|
||||
id.clone(),
|
||||
actual_name,
|
||||
file_storage_path,
|
||||
size,
|
||||
mime,
|
||||
folder_id,
|
||||
created_at,
|
||||
modified_at,
|
||||
)
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
self.persist_id_mapping(&id, &path_string).await.map_err(map_repo_err)?;
|
||||
self.persist_id_mapping(&id, &path_string)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
if let Some(parent) = abs_path.parent() {
|
||||
self.metadata_cache.invalidate_directory(parent).await;
|
||||
}
|
||||
@@ -286,45 +391,86 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
mut stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
||||
) -> Result<File, DomainError> {
|
||||
let folder_path = self.resolve_folder_path(&folder_id).await;
|
||||
let (file_storage_path, actual_name) = self.unique_file_path(&folder_path, &name).await.map_err(map_repo_err)?;
|
||||
let (file_storage_path, actual_name) = self
|
||||
.unique_file_path(&folder_path, &name)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
let abs_path = self.resolve_storage_path(&file_storage_path);
|
||||
self.ensure_parent_directory(&abs_path).await.map_err(map_repo_err)?;
|
||||
self.ensure_parent_directory(&abs_path)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
|
||||
let temp_path = abs_path.with_extension("tmp.upload");
|
||||
let mut fh = time::timeout(self.config.timeouts.file_timeout(), TokioFile::create(&temp_path))
|
||||
let mut fh = time::timeout(
|
||||
self.config.timeouts.file_timeout(),
|
||||
TokioFile::create(&temp_path),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout creating temp file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
let mut total_bytes: u64 = 0;
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let chunk = chunk_result.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.write_all(&chunk).await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let chunk =
|
||||
chunk_result.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.write_all(&chunk)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
total_bytes += chunk.len() as u64;
|
||||
}
|
||||
fh.flush().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.sync_all().await.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.flush()
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
fh.sync_all()
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
drop(fh);
|
||||
|
||||
// Atomic rename
|
||||
fs::rename(&temp_path, &abs_path).await
|
||||
fs::rename(&temp_path, &abs_path)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
let (size, created_at, modified_at) = self.get_file_metadata_raw(&abs_path).await.map_err(map_repo_err)?;
|
||||
let mime = if content_type.is_empty() { from_path(&abs_path).first_or_octet_stream().to_string() } else { content_type };
|
||||
let id = self.id_mapping_service.get_or_create_id(&file_storage_path).await
|
||||
let (size, created_at, modified_at) = self
|
||||
.get_file_metadata_raw(&abs_path)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
let mime = if content_type.is_empty() {
|
||||
from_path(&abs_path).first_or_octet_stream().to_string()
|
||||
} else {
|
||||
content_type
|
||||
};
|
||||
let id = self
|
||||
.id_mapping_service
|
||||
.get_or_create_id(&file_storage_path)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let path_string = file_storage_path.to_string();
|
||||
let log_name = actual_name.clone();
|
||||
|
||||
let file = File::with_timestamps(id.clone(), actual_name, file_storage_path, size, mime, folder_id, created_at, modified_at)
|
||||
let file = File::with_timestamps(
|
||||
id.clone(),
|
||||
actual_name,
|
||||
file_storage_path,
|
||||
size,
|
||||
mime,
|
||||
folder_id,
|
||||
created_at,
|
||||
modified_at,
|
||||
)
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
self.persist_id_mapping(&id, &path_string).await.map_err(map_repo_err)?;
|
||||
self.persist_id_mapping(&id, &path_string)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
if let Some(parent) = abs_path.parent() {
|
||||
self.metadata_cache.invalidate_directory(parent).await;
|
||||
}
|
||||
tracing::info!("✅ STREAMING UPLOAD COMPLETE: {} ({} bytes)", log_name, total_bytes);
|
||||
tracing::info!(
|
||||
"✅ STREAMING UPLOAD COMPLETE: {} ({} bytes)",
|
||||
log_name,
|
||||
total_bytes
|
||||
);
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
@@ -339,57 +485,87 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
if !old_abs.exists() || !old_abs.is_file() {
|
||||
return Err(DomainError::not_found("File", file_id.to_string()));
|
||||
}
|
||||
let (size, created_at, modified_at) = self.get_file_metadata_raw(&old_abs).await.map_err(map_repo_err)?;
|
||||
let name = original_path.file_name()
|
||||
let (size, created_at, modified_at) = self
|
||||
.get_file_metadata_raw(&old_abs)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
let name = original_path
|
||||
.file_name()
|
||||
.ok_or_else(|| DomainError::internal_error("File", "Invalid path"))?;
|
||||
let mime = from_path(&old_abs).first_or_octet_stream().to_string();
|
||||
|
||||
// Build target path
|
||||
let target_folder_path = self.resolve_folder_path(&target_folder_id).await;
|
||||
let new_storage_path = target_folder_path.join(&name);
|
||||
if self.file_exists_at_storage_path(&new_storage_path).await.map_err(map_repo_err)? {
|
||||
return Err(DomainError::already_exists("File",
|
||||
format!("File already exists at {}", new_storage_path.to_string())));
|
||||
if self
|
||||
.file_exists_at_storage_path(&new_storage_path)
|
||||
.await
|
||||
.map_err(map_repo_err)?
|
||||
{
|
||||
return Err(DomainError::already_exists(
|
||||
"File",
|
||||
format!("File already exists at {}", new_storage_path.to_string()),
|
||||
));
|
||||
}
|
||||
let new_abs = self.resolve_storage_path(&new_storage_path);
|
||||
self.ensure_parent_directory(&new_abs).await.map_err(map_repo_err)?;
|
||||
self.ensure_parent_directory(&new_abs)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
|
||||
// Rename
|
||||
time::timeout(
|
||||
self.config.timeouts.file_timeout(),
|
||||
FileSystemUtils::rename_with_sync(&old_abs, &new_abs),
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout moving file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
// Update mapping
|
||||
self.id_mapping_service.update_path(file_id, &new_storage_path).await?;
|
||||
self.id_mapping_service
|
||||
.update_path(file_id, &new_storage_path)
|
||||
.await?;
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
File::with_timestamps(file_id.to_string(), name, new_storage_path, size, mime, target_folder_id, created_at, modified_at)
|
||||
File::with_timestamps(
|
||||
file_id.to_string(),
|
||||
name,
|
||||
new_storage_path,
|
||||
size,
|
||||
mime,
|
||||
target_folder_id,
|
||||
created_at,
|
||||
modified_at,
|
||||
)
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))
|
||||
}
|
||||
|
||||
async fn rename_file(
|
||||
&self,
|
||||
file_id: &str,
|
||||
new_name: &str,
|
||||
) -> Result<File, DomainError> {
|
||||
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<File, DomainError> {
|
||||
// 1. Get current file info
|
||||
let original_path = self.id_mapping_service.get_path_by_id(file_id).await?;
|
||||
let old_abs = self.resolve_storage_path(&original_path);
|
||||
if !old_abs.exists() || !old_abs.is_file() {
|
||||
return Err(DomainError::not_found("File", file_id.to_string()));
|
||||
}
|
||||
let (size, created_at, modified_at) = self.get_file_metadata_raw(&old_abs).await.map_err(map_repo_err)?;
|
||||
let (size, created_at, modified_at) = self
|
||||
.get_file_metadata_raw(&old_abs)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
|
||||
// 2. Build new path (same parent directory, different filename)
|
||||
let parent = original_path.parent()
|
||||
let parent = original_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| StoragePath::new(vec![]));
|
||||
let new_storage_path = parent.join(new_name);
|
||||
if self.file_exists_at_storage_path(&new_storage_path).await.map_err(map_repo_err)? {
|
||||
return Err(DomainError::already_exists("File",
|
||||
format!("File already exists: {}", new_name)));
|
||||
if self
|
||||
.file_exists_at_storage_path(&new_storage_path)
|
||||
.await
|
||||
.map_err(map_repo_err)?
|
||||
{
|
||||
return Err(DomainError::already_exists(
|
||||
"File",
|
||||
format!("File already exists: {}", new_name),
|
||||
));
|
||||
}
|
||||
let new_abs = self.resolve_storage_path(&new_storage_path);
|
||||
let mime = from_path(&new_abs).first_or_octet_stream().to_string();
|
||||
@@ -398,12 +574,15 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
time::timeout(
|
||||
self.config.timeouts.file_timeout(),
|
||||
FileSystemUtils::rename_with_sync(&old_abs, &new_abs),
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.map_err(|_| DomainError::internal_error("File", "Timeout renaming file"))?
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
// 4. Update id→path mapping
|
||||
self.id_mapping_service.update_path(file_id, &new_storage_path).await?;
|
||||
self.id_mapping_service
|
||||
.update_path(file_id, &new_storage_path)
|
||||
.await?;
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
File::with_timestamps(
|
||||
@@ -428,7 +607,9 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
self.metadata_cache.invalidate_directory(parent).await;
|
||||
}
|
||||
|
||||
self.delete_file_non_blocking(abs_path).await.map_err(map_repo_err)?;
|
||||
self.delete_file_non_blocking(abs_path)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
|
||||
// Clean up the ID mapping so we don't leave orphaned entries
|
||||
if let Err(e) = self.id_mapping_service.remove_id(id).await {
|
||||
@@ -439,7 +620,11 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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> {
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(file_id).await?;
|
||||
let physical_path = self.resolve_storage_path(&storage_path);
|
||||
|
||||
@@ -460,9 +645,14 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
size: u64,
|
||||
) -> Result<(File, PathBuf), DomainError> {
|
||||
let folder_path = self.resolve_folder_path(&folder_id).await;
|
||||
let (file_storage_path, actual_name) = self.unique_file_path(&folder_path, &name).await.map_err(map_repo_err)?;
|
||||
let (file_storage_path, actual_name) = self
|
||||
.unique_file_path(&folder_path, &name)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
let abs_path = self.resolve_storage_path(&file_storage_path);
|
||||
self.ensure_parent_directory(&abs_path).await.map_err(map_repo_err)?;
|
||||
self.ensure_parent_directory(&abs_path)
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
|
||||
let mime = if content_type.is_empty() {
|
||||
from_path(&abs_path).first_or_octet_stream().to_string()
|
||||
@@ -474,11 +664,23 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
let id = self.id_mapping_service.get_or_create_id(&file_storage_path).await
|
||||
let id = self
|
||||
.id_mapping_service
|
||||
.get_or_create_id(&file_storage_path)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
let file = File::with_timestamps(id.clone(), actual_name, file_storage_path, size, mime, folder_id, now, now)
|
||||
let file = File::with_timestamps(
|
||||
id.clone(),
|
||||
actual_name,
|
||||
file_storage_path,
|
||||
size,
|
||||
mime,
|
||||
folder_id,
|
||||
now,
|
||||
now,
|
||||
)
|
||||
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
|
||||
|
||||
tracing::debug!("⚡ Registered deferred file: {} -> {:?}", id, abs_path);
|
||||
@@ -496,17 +698,21 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
|
||||
// Create trash directory
|
||||
let trash_dir = self.root_path.join(".trash").join("files");
|
||||
fs::create_dir_all(&trash_dir).await
|
||||
.map_err(|e| DomainError::internal_error("File", format!("Failed to create trash dir: {}", e)))?;
|
||||
fs::create_dir_all(&trash_dir).await.map_err(|e| {
|
||||
DomainError::internal_error("File", format!("Failed to create trash dir: {}", e))
|
||||
})?;
|
||||
|
||||
// Move file to trash
|
||||
let trash_path = trash_dir.join(file_id);
|
||||
fs::rename(&abs_path, &trash_path).await
|
||||
.map_err(|e| DomainError::internal_error("File", format!("Failed to move file to trash: {}", e)))?;
|
||||
fs::rename(&abs_path, &trash_path).await.map_err(|e| {
|
||||
DomainError::internal_error("File", format!("Failed to move file to trash: {}", e))
|
||||
})?;
|
||||
|
||||
// Update mapping to trash location
|
||||
let trash_storage_path = StoragePath::from_string(&format!(".trash/files/{}", file_id));
|
||||
self.id_mapping_service.update_path(file_id, &trash_storage_path).await?;
|
||||
self.id_mapping_service
|
||||
.update_path(file_id, &trash_storage_path)
|
||||
.await?;
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
// Invalidate cache
|
||||
@@ -515,36 +721,57 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
self.metadata_cache.invalidate_directory(parent).await;
|
||||
}
|
||||
|
||||
tracing::debug!("File moved to trash: {} -> {}", file_id, trash_path.display());
|
||||
tracing::debug!(
|
||||
"File moved to trash: {} -> {}",
|
||||
file_id,
|
||||
trash_path.display()
|
||||
);
|
||||
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> {
|
||||
// Get current path (should be in trash)
|
||||
let current_storage_path = self.id_mapping_service.get_path_by_id(file_id).await?;
|
||||
let current_abs_path = self.resolve_storage_path(¤t_storage_path);
|
||||
|
||||
if !current_abs_path.exists() {
|
||||
return Err(DomainError::not_found("File", format!("File {} not found in trash", file_id)));
|
||||
return Err(DomainError::not_found(
|
||||
"File",
|
||||
format!("File {} not found in trash", file_id),
|
||||
));
|
||||
}
|
||||
|
||||
// Ensure parent directory exists for original location
|
||||
let original_storage_path = StoragePath::from_string(original_path);
|
||||
let original_abs_path = self.resolve_storage_path(&original_storage_path);
|
||||
if let Some(parent) = original_abs_path.parent() {
|
||||
fs::create_dir_all(parent).await
|
||||
.map_err(|e| DomainError::internal_error("File", format!("Failed to create parent dir: {}", e)))?;
|
||||
fs::create_dir_all(parent).await.map_err(|e| {
|
||||
DomainError::internal_error("File", format!("Failed to create parent dir: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Move file back to original location
|
||||
fs::rename(¤t_abs_path, &original_abs_path).await
|
||||
.map_err(|e| DomainError::internal_error("File", format!("Failed to restore file: {}", e)))?;
|
||||
fs::rename(¤t_abs_path, &original_abs_path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error("File", format!("Failed to restore file: {}", e))
|
||||
})?;
|
||||
|
||||
// Update mapping back to original path
|
||||
self.id_mapping_service.update_path(file_id, &original_storage_path).await?;
|
||||
self.id_mapping_service
|
||||
.update_path(file_id, &original_storage_path)
|
||||
.await?;
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
tracing::debug!("File restored from trash: {} -> {}", file_id, original_abs_path.display());
|
||||
tracing::debug!(
|
||||
"File restored from trash: {} -> {}",
|
||||
file_id,
|
||||
original_abs_path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -555,7 +782,9 @@ impl FileWritePort for FileFsWriteRepository {
|
||||
|
||||
// Delete the physical file if it exists
|
||||
if abs_path.exists() {
|
||||
self.delete_file_non_blocking(abs_path.clone()).await.map_err(map_repo_err)?;
|
||||
self.delete_file_non_blocking(abs_path.clone())
|
||||
.await
|
||||
.map_err(map_repo_err)?;
|
||||
}
|
||||
|
||||
// Remove ID mapping
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
use async_trait::async_trait;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use async_trait::async_trait;
|
||||
use tokio::fs;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::domain::entities::folder::{Folder, FolderError};
|
||||
use crate::infrastructure::repositories::repository_errors::{
|
||||
FolderRepositoryError, FolderRepositoryResult
|
||||
};
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::infrastructure::repositories::repository_errors::{
|
||||
FolderRepositoryError, FolderRepositoryResult,
|
||||
};
|
||||
use crate::infrastructure::services::path_service::PathService;
|
||||
// use crate::application::ports::outbound::IdMappingPort;
|
||||
use crate::infrastructure::services::id_mapping_service::{IdMappingService, IdMappingError};
|
||||
use crate::application::services::storage_mediator::StorageMediator;
|
||||
use crate::domain::repositories::folder_repository::FolderRepository;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::repositories::folder_repository::FolderRepository;
|
||||
use crate::infrastructure::services::id_mapping_service::{IdMappingError, IdMappingService};
|
||||
|
||||
// To be able to use streams in the list_folders function
|
||||
use tokio_stream;
|
||||
@@ -57,14 +57,11 @@ impl FolderFsRepository {
|
||||
|
||||
// Create minimal implementations just to satisfy initialization
|
||||
// Since we can't easily block on an async function in a sync context, create with a stub
|
||||
let id_mapping_service = Arc::new(
|
||||
IdMappingService::new_sync(root_path.clone())
|
||||
);
|
||||
let id_mapping_service = Arc::new(IdMappingService::new_sync(root_path.clone()));
|
||||
|
||||
// Create a self-referential stub (only used for initialization)
|
||||
let storage_mediator_stub = Arc::new(
|
||||
crate::application::services::storage_mediator::StubStorageMediator::new()
|
||||
);
|
||||
let storage_mediator_stub =
|
||||
Arc::new(crate::application::services::storage_mediator::StubStorageMediator::new());
|
||||
|
||||
Self {
|
||||
root_path,
|
||||
@@ -80,14 +77,12 @@ impl FolderFsRepository {
|
||||
|
||||
// Timeout to avoid blocking
|
||||
let read_dir_timeout = Duration::from_secs(30);
|
||||
let read_dir_result = timeout(
|
||||
read_dir_timeout,
|
||||
read_dir(directory_path)
|
||||
).await;
|
||||
let read_dir_result = timeout(read_dir_timeout, read_dir(directory_path)).await;
|
||||
|
||||
match read_dir_result {
|
||||
Ok(result) => {
|
||||
let mut entries = result.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?;
|
||||
let mut entries =
|
||||
result.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?;
|
||||
let mut count = 0;
|
||||
|
||||
// Count entries manually, skipping hidden/system directories
|
||||
@@ -100,12 +95,11 @@ impl FolderFsRepository {
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
},
|
||||
Err(_) => {
|
||||
Err(FolderRepositoryError::Other(
|
||||
format!("Timeout counting items in directory: {}", directory_path.display())
|
||||
))
|
||||
}
|
||||
Err(_) => Err(FolderRepositoryError::Other(format!(
|
||||
"Timeout counting items in directory: {}",
|
||||
directory_path.display()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,45 +109,74 @@ impl FolderFsRepository {
|
||||
}
|
||||
|
||||
/// Returns a reference to the ID mapping service
|
||||
pub fn id_mapping_service(&self) -> &Arc<dyn crate::application::ports::outbound::IdMappingPort> {
|
||||
pub fn id_mapping_service(
|
||||
&self,
|
||||
) -> &Arc<dyn crate::application::ports::outbound::IdMappingPort> {
|
||||
&self.id_mapping_service
|
||||
}
|
||||
|
||||
/// Gets the storage path for a folder by its ID (internal helper)
|
||||
async fn _get_folder_storage_path(&self, id: &str) -> FolderRepositoryResult<StoragePath> {
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(id).await
|
||||
let storage_path = self
|
||||
.id_mapping_service
|
||||
.get_path_by_id(id)
|
||||
.await
|
||||
.map_err(FolderRepositoryError::from)?;
|
||||
Ok(storage_path)
|
||||
}
|
||||
|
||||
/// Gets a folder path from the ID mapping service
|
||||
pub async fn get_mapped_folder_path(&self, folder_id: &str) -> FolderRepositoryResult<String> {
|
||||
let storage_path = self.id_mapping_service.get_path_by_id(folder_id).await
|
||||
.map_err(|e| FolderRepositoryError::StorageError(format!("Failed to get folder path: {}", e)))?;
|
||||
let storage_path = self
|
||||
.id_mapping_service
|
||||
.get_path_by_id(folder_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
FolderRepositoryError::StorageError(format!("Failed to get folder path: {}", e))
|
||||
})?;
|
||||
Ok(storage_path.to_string())
|
||||
}
|
||||
|
||||
/// Updates a folder path in the ID mapping service
|
||||
pub async fn update_mapped_folder_path(&self, folder_id: &str, new_path: &PathBuf) -> FolderRepositoryResult<()> {
|
||||
pub async fn update_mapped_folder_path(
|
||||
&self,
|
||||
folder_id: &str,
|
||||
new_path: &PathBuf,
|
||||
) -> FolderRepositoryResult<()> {
|
||||
let storage_path = StoragePath::from_string(new_path.to_string_lossy().as_ref());
|
||||
self.id_mapping_service.update_path(folder_id, &storage_path).await
|
||||
.map_err(|e| FolderRepositoryError::StorageError(format!("Failed to update folder path: {}", e)))
|
||||
self.id_mapping_service
|
||||
.update_path(folder_id, &storage_path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
FolderRepositoryError::StorageError(format!("Failed to update folder path: {}", e))
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes a folder ID from the ID mapping service
|
||||
pub async fn remove_mapped_folder_id(&self, folder_id: &str) -> FolderRepositoryResult<()> {
|
||||
self.id_mapping_service.remove_id(folder_id).await
|
||||
.map_err(|e| FolderRepositoryError::StorageError(format!("Failed to remove folder ID: {}", e)))
|
||||
self.id_mapping_service
|
||||
.remove_id(folder_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
FolderRepositoryError::StorageError(format!("Failed to remove folder ID: {}", e))
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if a folder exists at a given storage path
|
||||
async fn check_folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult<bool> {
|
||||
async fn check_folder_exists_at_storage_path(
|
||||
&self,
|
||||
storage_path: &StoragePath,
|
||||
) -> FolderRepositoryResult<bool> {
|
||||
let abs_path = self.resolve_storage_path(storage_path);
|
||||
|
||||
// Check if folder exists and is a directory
|
||||
let exists = abs_path.exists() && abs_path.is_dir();
|
||||
|
||||
tracing::debug!("Checking if folder exists: {} - path: {}", exists, abs_path.display());
|
||||
tracing::debug!(
|
||||
"Checking if folder exists: {} - path: {}",
|
||||
exists,
|
||||
abs_path.display()
|
||||
);
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
@@ -175,45 +198,44 @@ impl FolderFsRepository {
|
||||
) -> FolderRepositoryResult<Folder> {
|
||||
// If timestamps are provided, use them; otherwise, let Folder::new create default timestamps
|
||||
let folder = if let (Some(created), Some(modified)) = (created_at, modified_at) {
|
||||
Folder::with_timestamps(
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
parent_id,
|
||||
created,
|
||||
modified,
|
||||
)
|
||||
Folder::with_timestamps(id, name, storage_path, parent_id, created, modified)
|
||||
} else {
|
||||
Folder::new(
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
parent_id,
|
||||
)
|
||||
Folder::new(id, name, storage_path, parent_id)
|
||||
};
|
||||
|
||||
// Convert domain error to repository error
|
||||
folder.map_err(|e| match e {
|
||||
FolderError::InvalidFolderName(name) =>
|
||||
FolderRepositoryError::ValidationError(format!("Invalid folder name: {}", name)),
|
||||
FolderError::ValidationError(msg) =>
|
||||
FolderRepositoryError::ValidationError(msg),
|
||||
FolderError::InvalidFolderName(name) => {
|
||||
FolderRepositoryError::ValidationError(format!("Invalid folder name: {}", name))
|
||||
}
|
||||
FolderError::ValidationError(msg) => FolderRepositoryError::ValidationError(msg),
|
||||
})
|
||||
}
|
||||
|
||||
/// Extracts folder metadata from a physical path
|
||||
async fn get_folder_metadata(&self, abs_path: &PathBuf) -> FolderRepositoryResult<(u64, u64)> {
|
||||
let metadata = fs::metadata(&abs_path).await
|
||||
let metadata = fs::metadata(&abs_path)
|
||||
.await
|
||||
.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
// Get creation timestamp
|
||||
let created_at = metadata.created()
|
||||
.map(|time| time.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
let created_at = metadata
|
||||
.created()
|
||||
.map(|time| {
|
||||
time.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
})
|
||||
.unwrap_or_else(|_| 0);
|
||||
|
||||
// Get modification timestamp
|
||||
let modified_at = metadata.modified()
|
||||
.map(|time| time.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
|
||||
let modified_at = metadata
|
||||
.modified()
|
||||
.map(|time| {
|
||||
time.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
})
|
||||
.unwrap_or_else(|_| 0);
|
||||
|
||||
Ok((created_at, modified_at))
|
||||
@@ -226,7 +248,9 @@ impl From<IdMappingError> for FolderRepositoryError {
|
||||
match err {
|
||||
IdMappingError::NotFound(id) => FolderRepositoryError::NotFound(id),
|
||||
IdMappingError::IoError(e) => FolderRepositoryError::StorageError(e.to_string()),
|
||||
IdMappingError::Timeout(msg) => FolderRepositoryError::StorageError(format!("Timeout: {}", msg)),
|
||||
IdMappingError::Timeout(msg) => {
|
||||
FolderRepositoryError::StorageError(format!("Timeout: {}", msg))
|
||||
}
|
||||
_ => FolderRepositoryError::StorageError(err.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -247,19 +271,25 @@ impl Clone for FolderFsRepository {
|
||||
|
||||
#[async_trait]
|
||||
impl FolderRepository for FolderFsRepository {
|
||||
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> {
|
||||
// Get the parent folder path (if any)
|
||||
let parent_storage_path = match &parent_id {
|
||||
Some(id) => {
|
||||
match self._get_folder_storage_path(id).await {
|
||||
Some(id) => match self._get_folder_storage_path(id).await {
|
||||
Ok(path) => {
|
||||
tracing::info!("Using folder path: {:?} for parent_id: {:?}", path.to_string(), id);
|
||||
tracing::info!(
|
||||
"Using folder path: {:?} for parent_id: {:?}",
|
||||
path.to_string(),
|
||||
id
|
||||
);
|
||||
Some(path)
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting parent folder: {}", e);
|
||||
return Err(DomainError::from(e));
|
||||
},
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
@@ -270,37 +300,58 @@ impl FolderRepository for FolderFsRepository {
|
||||
Some(parent) => parent.join(&name),
|
||||
None => StoragePath::from_string(&name),
|
||||
};
|
||||
tracing::info!("Creating folder at path: {:?}", folder_storage_path.to_string());
|
||||
tracing::info!(
|
||||
"Creating folder at path: {:?}",
|
||||
folder_storage_path.to_string()
|
||||
);
|
||||
|
||||
// Check if folder already exists
|
||||
if self.check_folder_exists_at_storage_path(&folder_storage_path).await.map_err(DomainError::from)? {
|
||||
return Err(DomainError::already_exists("Folder", folder_storage_path.to_string()));
|
||||
if self
|
||||
.check_folder_exists_at_storage_path(&folder_storage_path)
|
||||
.await
|
||||
.map_err(DomainError::from)?
|
||||
{
|
||||
return Err(DomainError::already_exists(
|
||||
"Folder",
|
||||
folder_storage_path.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Create the physical directory
|
||||
let abs_path = self.resolve_storage_path(&folder_storage_path);
|
||||
self.create_directory(&abs_path).await
|
||||
self.create_directory(&abs_path)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?;
|
||||
|
||||
// Create and return the folder entity with a persisted ID
|
||||
let id = self.id_mapping_service.get_or_create_id(&folder_storage_path).await
|
||||
let id = self
|
||||
.id_mapping_service
|
||||
.get_or_create_id(&folder_storage_path)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?;
|
||||
let folder = self.create_folder_entity(
|
||||
let folder = self
|
||||
.create_folder_entity(
|
||||
id.clone(),
|
||||
name.clone(),
|
||||
folder_storage_path.clone(),
|
||||
parent_id.clone(),
|
||||
None,
|
||||
None,
|
||||
).await.map_err(DomainError::from)?;
|
||||
)
|
||||
.await
|
||||
.map_err(DomainError::from)?;
|
||||
|
||||
// Ensure ID mapping is persisted
|
||||
let save_result = self.id_mapping_service.save_changes().await;
|
||||
if let Err(e) = &save_result {
|
||||
tracing::error!("Failed to save ID mapping for folder {}: {}", id, e);
|
||||
} else {
|
||||
tracing::info!("Successfully saved ID mapping for folder ID: {} -> path: {} (name: {})",
|
||||
id, folder_storage_path.to_string(), name);
|
||||
tracing::info!(
|
||||
"Successfully saved ID mapping for folder ID: {} -> path: {} (name: {})",
|
||||
id,
|
||||
folder_storage_path.to_string(),
|
||||
name
|
||||
);
|
||||
}
|
||||
save_result?;
|
||||
|
||||
@@ -318,18 +369,27 @@ impl FolderRepository for FolderFsRepository {
|
||||
let abs_path = self.resolve_storage_path(&storage_path);
|
||||
if !abs_path.exists() || !abs_path.is_dir() {
|
||||
tracing::error!("Folder not found at path: {}", abs_path.display());
|
||||
return Err(DomainError::not_found("Folder", format!("Folder {} not found at {}", id, storage_path.to_string())));
|
||||
return Err(DomainError::not_found(
|
||||
"Folder",
|
||||
format!("Folder {} not found at {}", id, storage_path.to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
// Get folder metadata
|
||||
let (created_at, modified_at) = self.get_folder_metadata(&abs_path).await.map_err(DomainError::from)?;
|
||||
let (created_at, modified_at) = self
|
||||
.get_folder_metadata(&abs_path)
|
||||
.await
|
||||
.map_err(DomainError::from)?;
|
||||
|
||||
// Get folder name from the storage path
|
||||
let name = match storage_path.file_name() {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
tracing::error!("Invalid folder path: {}", storage_path.to_string());
|
||||
return Err(DomainError::validation_error(format!("Invalid path: {}", storage_path.to_string())));
|
||||
return Err(DomainError::validation_error(format!(
|
||||
"Invalid path: {}",
|
||||
storage_path.to_string()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -338,18 +398,24 @@ impl FolderRepository for FolderFsRepository {
|
||||
let parent_id: Option<String> = if parent.is_none() || parent.as_ref().unwrap().is_empty() {
|
||||
None
|
||||
} else {
|
||||
self.id_mapping_service.get_or_create_id(parent.as_ref().unwrap()).await.ok()
|
||||
self.id_mapping_service
|
||||
.get_or_create_id(parent.as_ref().unwrap())
|
||||
.await
|
||||
.ok()
|
||||
};
|
||||
|
||||
// Create folder entity
|
||||
let folder = self.create_folder_entity(
|
||||
let folder = self
|
||||
.create_folder_entity(
|
||||
id.to_string(),
|
||||
name,
|
||||
storage_path,
|
||||
parent_id,
|
||||
Some(created_at),
|
||||
Some(modified_at),
|
||||
).await.map_err(DomainError::from)?;
|
||||
)
|
||||
.await
|
||||
.map_err(DomainError::from)?;
|
||||
|
||||
Ok(folder)
|
||||
}
|
||||
@@ -365,7 +431,10 @@ impl FolderRepository for FolderFsRepository {
|
||||
let name = match storage_path.file_name() {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
return Err(DomainError::validation_error(format!("Invalid path: {}", storage_path.to_string())));
|
||||
return Err(DomainError::validation_error(format!(
|
||||
"Invalid path: {}",
|
||||
storage_path.to_string()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -374,25 +443,41 @@ impl FolderRepository for FolderFsRepository {
|
||||
let parent_id: Option<String> = if parent.is_none() || parent.as_ref().unwrap().is_empty() {
|
||||
None
|
||||
} else {
|
||||
self.id_mapping_service.get_or_create_id(parent.as_ref().unwrap()).await.ok()
|
||||
self.id_mapping_service
|
||||
.get_or_create_id(parent.as_ref().unwrap())
|
||||
.await
|
||||
.ok()
|
||||
};
|
||||
|
||||
// Get folder metadata
|
||||
let (created_at, modified_at) = self.get_folder_metadata(&abs_path).await.map_err(DomainError::from)?;
|
||||
let (created_at, modified_at) = self
|
||||
.get_folder_metadata(&abs_path)
|
||||
.await
|
||||
.map_err(DomainError::from)?;
|
||||
|
||||
// Get or create an ID for this path
|
||||
let id = self.id_mapping_service.get_or_create_id(storage_path).await?;
|
||||
tracing::debug!("Found folder with path: {:?}, assigned ID: {}", storage_path.to_string(), id);
|
||||
let id = self
|
||||
.id_mapping_service
|
||||
.get_or_create_id(storage_path)
|
||||
.await?;
|
||||
tracing::debug!(
|
||||
"Found folder with path: {:?}, assigned ID: {}",
|
||||
storage_path.to_string(),
|
||||
id
|
||||
);
|
||||
|
||||
// Create folder entity
|
||||
let folder = self.create_folder_entity(
|
||||
let folder = self
|
||||
.create_folder_entity(
|
||||
id,
|
||||
name,
|
||||
storage_path.clone(),
|
||||
parent_id,
|
||||
Some(created_at),
|
||||
Some(modified_at),
|
||||
).await.map_err(DomainError::from)?;
|
||||
)
|
||||
.await
|
||||
.map_err(DomainError::from)?;
|
||||
|
||||
// Ensure ID mapping is persisted
|
||||
self.id_mapping_service.save_changes().await?;
|
||||
@@ -402,22 +487,20 @@ impl FolderRepository for FolderFsRepository {
|
||||
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError> {
|
||||
use futures::stream::StreamExt;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
tracing::info!("Listing folders in parent_id: {:?}", parent_id);
|
||||
|
||||
// Get the parent storage path
|
||||
let parent_storage_path = match parent_id {
|
||||
Some(id) => {
|
||||
match self._get_folder_storage_path(id).await {
|
||||
Some(id) => match self._get_folder_storage_path(id).await {
|
||||
Ok(path) => {
|
||||
tracing::info!("Found parent folder with path: {:?}", path.to_string());
|
||||
path
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting parent folder by ID: {}: {}", id, e);
|
||||
return Ok(Vec::new());
|
||||
},
|
||||
}
|
||||
},
|
||||
None => StoragePath::root(),
|
||||
@@ -429,20 +512,24 @@ impl FolderRepository for FolderFsRepository {
|
||||
|
||||
// Ensure the directory exists
|
||||
if !abs_parent_path.exists() || !abs_parent_path.is_dir() {
|
||||
tracing::error!("Directory does not exist or is not a directory: {:?}", &abs_parent_path);
|
||||
tracing::error!(
|
||||
"Directory does not exist or is not a directory: {:?}",
|
||||
&abs_parent_path
|
||||
);
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Read the directory with a timeout
|
||||
let read_dir_timeout = Duration::from_secs(30);
|
||||
let read_dir_result = match timeout(
|
||||
read_dir_timeout,
|
||||
fs::read_dir(&abs_parent_path)
|
||||
).await {
|
||||
Ok(result) => result.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?,
|
||||
let read_dir_result = match timeout(read_dir_timeout, fs::read_dir(&abs_parent_path)).await
|
||||
{
|
||||
Ok(result) => {
|
||||
result.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(DomainError::internal_error("Folder",
|
||||
format!("Timeout reading directory: {}", abs_parent_path.display())
|
||||
return Err(DomainError::internal_error(
|
||||
"Folder",
|
||||
format!("Timeout reading directory: {}", abs_parent_path.display()),
|
||||
));
|
||||
}
|
||||
};
|
||||
@@ -462,7 +549,11 @@ impl FolderRepository for FolderFsRepository {
|
||||
let metadata = match entry.metadata().await {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
tracing::error!("Error getting metadata for {}: {}", entry.path().display(), err);
|
||||
tracing::error!(
|
||||
"Error getting metadata for {}: {}",
|
||||
entry.path().display(),
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -483,20 +574,19 @@ impl FolderRepository for FolderFsRepository {
|
||||
let get_folder_timeout = Duration::from_secs(5);
|
||||
let folder_result = timeout(
|
||||
get_folder_timeout,
|
||||
self.get_folder_by_path(&folder_storage_path)
|
||||
).await;
|
||||
self.get_folder_by_path(&folder_storage_path),
|
||||
)
|
||||
.await;
|
||||
|
||||
match folder_result {
|
||||
Ok(result) => {
|
||||
match result {
|
||||
Ok(result) => match result {
|
||||
Ok(folder) => {
|
||||
tracing::debug!("Found folder: {}", folder.name());
|
||||
folders.push(folder);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Could not get folder entity for {}: {}", folder_name, e);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
tracing::warn!("Timeout getting folder entity for {}", folder_name);
|
||||
@@ -517,22 +607,24 @@ impl FolderRepository for FolderFsRepository {
|
||||
parent_id: Option<&str>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
include_total: bool
|
||||
include_total: bool,
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError> {
|
||||
use futures::stream::StreamExt;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
tracing::info!("Listing folders in parent_id: {:?} with pagination (offset={}, limit={})",
|
||||
parent_id, offset, limit);
|
||||
tracing::info!(
|
||||
"Listing folders in parent_id: {:?} with pagination (offset={}, limit={})",
|
||||
parent_id,
|
||||
offset,
|
||||
limit
|
||||
);
|
||||
|
||||
let parent_storage_path = match parent_id {
|
||||
Some(id) => {
|
||||
match self._get_folder_storage_path(id).await {
|
||||
Some(id) => match self._get_folder_storage_path(id).await {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting parent folder by ID: {}: {}", id, e);
|
||||
return Ok((Vec::new(), Some(0)));
|
||||
},
|
||||
}
|
||||
},
|
||||
None => StoragePath::root(),
|
||||
@@ -557,14 +649,15 @@ impl FolderRepository for FolderFsRepository {
|
||||
};
|
||||
|
||||
let read_dir_timeout = Duration::from_secs(30);
|
||||
let read_dir_result = match timeout(
|
||||
read_dir_timeout,
|
||||
fs::read_dir(&abs_parent_path)
|
||||
).await {
|
||||
Ok(result) => result.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?,
|
||||
let read_dir_result = match timeout(read_dir_timeout, fs::read_dir(&abs_parent_path)).await
|
||||
{
|
||||
Ok(result) => {
|
||||
result.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(DomainError::internal_error("Folder",
|
||||
format!("Timeout reading directory: {}", abs_parent_path.display())
|
||||
return Err(DomainError::internal_error(
|
||||
"Folder",
|
||||
format!("Timeout reading directory: {}", abs_parent_path.display()),
|
||||
));
|
||||
}
|
||||
};
|
||||
@@ -623,18 +716,20 @@ impl FolderRepository for FolderFsRepository {
|
||||
}
|
||||
};
|
||||
|
||||
let folder_result = timeout(
|
||||
Duration::from_secs(10),
|
||||
self.get_folder_by_path(&rel_path)
|
||||
).await;
|
||||
let folder_result =
|
||||
timeout(Duration::from_secs(10), self.get_folder_by_path(&rel_path)).await;
|
||||
|
||||
match folder_result {
|
||||
Ok(result) => match result {
|
||||
Ok(folder) => {
|
||||
folders.push(folder);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting folder by path: {}: {}", rel_path.to_string(), e);
|
||||
tracing::error!(
|
||||
"Error getting folder by path: {}: {}",
|
||||
rel_path.to_string(),
|
||||
e
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
@@ -646,7 +741,8 @@ impl FolderRepository for FolderFsRepository {
|
||||
}
|
||||
|
||||
if !folders.is_empty()
|
||||
&& let Err(e) = self.id_mapping_service.save_changes().await {
|
||||
&& let Err(e) = self.id_mapping_service.save_changes().await
|
||||
{
|
||||
tracing::error!("Error saving ID mappings: {}", e);
|
||||
}
|
||||
|
||||
@@ -655,31 +751,58 @@ impl FolderRepository for FolderFsRepository {
|
||||
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError> {
|
||||
let original_folder = self.get_folder(id).await?;
|
||||
tracing::debug!("Renaming folder with ID: {}, Name: {}", id, original_folder.name());
|
||||
tracing::debug!(
|
||||
"Renaming folder with ID: {}, Name: {}",
|
||||
id,
|
||||
original_folder.name()
|
||||
);
|
||||
|
||||
let renamed_folder = original_folder.with_name(new_name)
|
||||
let renamed_folder = original_folder
|
||||
.with_name(new_name)
|
||||
.map_err(|e| DomainError::validation_error(e.to_string()))?;
|
||||
|
||||
if self.check_folder_exists_at_storage_path(renamed_folder.storage_path()).await.map_err(DomainError::from)? {
|
||||
return Err(DomainError::already_exists("Folder", renamed_folder.storage_path().to_string()));
|
||||
if self
|
||||
.check_folder_exists_at_storage_path(renamed_folder.storage_path())
|
||||
.await
|
||||
.map_err(DomainError::from)?
|
||||
{
|
||||
return Err(DomainError::already_exists(
|
||||
"Folder",
|
||||
renamed_folder.storage_path().to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let abs_old_path = self.resolve_storage_path(original_folder.storage_path());
|
||||
let abs_new_path = self.resolve_storage_path(renamed_folder.storage_path());
|
||||
|
||||
fs::rename(&abs_old_path, &abs_new_path).await
|
||||
fs::rename(&abs_old_path, &abs_new_path)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?;
|
||||
|
||||
self.id_mapping_service.update_path(id, renamed_folder.storage_path()).await?;
|
||||
self.id_mapping_service
|
||||
.update_path(id, renamed_folder.storage_path())
|
||||
.await?;
|
||||
self.id_mapping_service.save_changes().await?;
|
||||
|
||||
tracing::debug!("Folder renamed successfully: ID={}, New name={}", id, renamed_folder.name());
|
||||
tracing::debug!(
|
||||
"Folder renamed successfully: ID={}, New name={}",
|
||||
id,
|
||||
renamed_folder.name()
|
||||
);
|
||||
Ok(renamed_folder)
|
||||
}
|
||||
|
||||
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> {
|
||||
let original_folder = self.get_folder(id).await?;
|
||||
tracing::debug!("Moving folder with ID: {}, Name: {}", id, original_folder.name());
|
||||
tracing::debug!(
|
||||
"Moving folder with ID: {}, Name: {}",
|
||||
id,
|
||||
original_folder.name()
|
||||
);
|
||||
|
||||
if original_folder.parent_id() == new_parent_id {
|
||||
tracing::info!("Folder is already in the target parent, no need to move");
|
||||
@@ -687,26 +810,34 @@ impl FolderRepository for FolderFsRepository {
|
||||
}
|
||||
|
||||
let target_parent_storage_path = match new_parent_id {
|
||||
Some(parent_id) => {
|
||||
match self._get_folder_storage_path(parent_id).await {
|
||||
Some(parent_id) => match self._get_folder_storage_path(parent_id).await {
|
||||
Ok(path) => Some(path),
|
||||
Err(e) => {
|
||||
return Err(DomainError::internal_error("Folder",
|
||||
format!("Could not get target folder: {}", e)
|
||||
return Err(DomainError::internal_error(
|
||||
"Folder",
|
||||
format!("Could not get target folder: {}", e),
|
||||
));
|
||||
}
|
||||
}
|
||||
},
|
||||
None => None
|
||||
None => None,
|
||||
};
|
||||
|
||||
let new_parent_id_option = new_parent_id.map(String::from);
|
||||
let moved_folder = original_folder.with_parent(new_parent_id_option, target_parent_storage_path)
|
||||
let moved_folder = original_folder
|
||||
.with_parent(new_parent_id_option, target_parent_storage_path)
|
||||
.map_err(|e| DomainError::validation_error(e.to_string()))?;
|
||||
|
||||
if self.check_folder_exists_at_storage_path(moved_folder.storage_path()).await.map_err(DomainError::from)? {
|
||||
return Err(DomainError::already_exists("Folder",
|
||||
format!("Folder already exists at destination: {}", moved_folder.storage_path().to_string())
|
||||
if self
|
||||
.check_folder_exists_at_storage_path(moved_folder.storage_path())
|
||||
.await
|
||||
.map_err(DomainError::from)?
|
||||
{
|
||||
return Err(DomainError::already_exists(
|
||||
"Folder",
|
||||
format!(
|
||||
"Folder already exists at destination: {}",
|
||||
moved_folder.storage_path().to_string()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -714,22 +845,30 @@ impl FolderRepository for FolderFsRepository {
|
||||
let new_abs_path = self.resolve_storage_path(moved_folder.storage_path());
|
||||
|
||||
if let Some(parent) = new_abs_path.parent() {
|
||||
fs::create_dir_all(parent).await
|
||||
fs::create_dir_all(parent)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?;
|
||||
}
|
||||
|
||||
fs::rename(&old_abs_path, &new_abs_path).await
|
||||
fs::rename(&old_abs_path, &new_abs_path)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("Folder", e.to_string()))?;
|
||||
|
||||
self.id_mapping_service.update_path(id, moved_folder.storage_path()).await?;
|
||||
self.id_mapping_service
|
||||
.update_path(id, moved_folder.storage_path())
|
||||
.await?;
|
||||
self.id_mapping_service.save_changes().await?;
|
||||
|
||||
tracing::debug!("Folder moved successfully: ID={}, New path={:?}", id, moved_folder.storage_path().to_string());
|
||||
tracing::debug!(
|
||||
"Folder moved successfully: ID={}, New path={:?}",
|
||||
id,
|
||||
moved_folder.storage_path().to_string()
|
||||
);
|
||||
Ok(moved_folder)
|
||||
}
|
||||
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> {
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
let folder = self.get_folder(id).await?;
|
||||
let folder_name = folder.name().to_string();
|
||||
@@ -756,7 +895,9 @@ impl FolderRepository for FolderFsRepository {
|
||||
}
|
||||
}
|
||||
count
|
||||
}).await.unwrap_or(0);
|
||||
})
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
if entry_count > 1000 {
|
||||
tracing::info!("Large folder detected with >1000 entries, using blocking removal");
|
||||
@@ -764,15 +905,19 @@ impl FolderRepository for FolderFsRepository {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Err(e) = std::fs::remove_dir_all(&path_for_large_removal) {
|
||||
tracing::error!("Error removing large directory: {}", e);
|
||||
return Err(std::io::Error::other(
|
||||
format!("Failed to remove large directory: {}", e)
|
||||
));
|
||||
return Err(std::io::Error::other(format!(
|
||||
"Failed to remove large directory: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}).await.unwrap_or_else(|e| {
|
||||
Err(std::io::Error::other(
|
||||
format!("Task panicked during directory removal: {}", e)
|
||||
))
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
Err(std::io::Error::other(format!(
|
||||
"Task panicked during directory removal: {}",
|
||||
e
|
||||
)))
|
||||
})
|
||||
} else {
|
||||
fs::remove_dir_all(&path_for_deletion).await
|
||||
@@ -781,24 +926,20 @@ impl FolderRepository for FolderFsRepository {
|
||||
|
||||
const DELETE_TIMEOUT_SECS: u64 = 60;
|
||||
|
||||
let delete_result = timeout(
|
||||
Duration::from_secs(DELETE_TIMEOUT_SECS),
|
||||
delete_task
|
||||
).await;
|
||||
let delete_result = timeout(Duration::from_secs(DELETE_TIMEOUT_SECS), delete_task).await;
|
||||
|
||||
match delete_result {
|
||||
Ok(task_result) => {
|
||||
match task_result {
|
||||
Ok(task_result) => match task_result {
|
||||
Ok(fs_result) => {
|
||||
if let Err(e) = fs_result {
|
||||
return Err(DomainError::internal_error("Folder", e.to_string()));
|
||||
}
|
||||
},
|
||||
Err(join_err) => {
|
||||
return Err(DomainError::internal_error("Folder",
|
||||
format!("Task panicked during folder deletion: {}", join_err)
|
||||
));
|
||||
}
|
||||
Err(join_err) => {
|
||||
return Err(DomainError::internal_error(
|
||||
"Folder",
|
||||
format!("Task panicked during folder deletion: {}", join_err),
|
||||
));
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
@@ -809,41 +950,61 @@ impl FolderRepository for FolderFsRepository {
|
||||
const MAPPING_TIMEOUT_SECS: u64 = 5;
|
||||
let remove_id_result = timeout(
|
||||
Duration::from_secs(MAPPING_TIMEOUT_SECS),
|
||||
self.id_mapping_service.remove_id(id)
|
||||
).await;
|
||||
self.id_mapping_service.remove_id(id),
|
||||
)
|
||||
.await;
|
||||
|
||||
match remove_id_result {
|
||||
Ok(result) => result?,
|
||||
Err(_) => {
|
||||
return Err(DomainError::internal_error("Folder",
|
||||
"Timeout removing folder ID from mapping".to_string()
|
||||
return Err(DomainError::internal_error(
|
||||
"Folder",
|
||||
"Timeout removing folder ID from mapping".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.id_mapping_service.save_changes().await;
|
||||
|
||||
tracing::info!("Folder deleted successfully: ID={}, Name={}", id, folder_name);
|
||||
tracing::info!(
|
||||
"Folder deleted successfully: ID={}, Name={}",
|
||||
id,
|
||||
folder_name
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
self.check_folder_exists_at_storage_path(storage_path).await.map_err(DomainError::from)
|
||||
self.check_folder_exists_at_storage_path(storage_path)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError> {
|
||||
self._get_folder_storage_path(id).await.map_err(DomainError::from)
|
||||
self._get_folder_storage_path(id)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError> {
|
||||
self._trash_move_to_trash(folder_id).await.map_err(DomainError::from)
|
||||
self._trash_move_to_trash(folder_id)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> Result<(), DomainError> {
|
||||
self._trash_restore_from_trash(folder_id, original_path).await.map_err(DomainError::from)
|
||||
async fn restore_from_trash(
|
||||
&self,
|
||||
folder_id: &str,
|
||||
original_path: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
self._trash_restore_from_trash(folder_id, original_path)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError> {
|
||||
self._trash_delete_folder_permanently(folder_id).await.map_err(DomainError::from)
|
||||
self._trash_delete_folder_permanently(folder_id)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::infrastructure::repositories::repository_errors::FolderRepositoryResult;
|
||||
use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository;
|
||||
use crate::infrastructure::repositories::repository_errors::FolderRepositoryResult;
|
||||
|
||||
// This file contains the implementation of trash-related methods
|
||||
// for the FolderFsRepository folder repository
|
||||
@@ -21,7 +21,8 @@ impl FolderFsRepository {
|
||||
|
||||
// Ensure the trash directory exists
|
||||
if !trash_dir.exists() {
|
||||
fs::create_dir_all(&trash_dir).await
|
||||
fs::create_dir_all(&trash_dir)
|
||||
.await
|
||||
.map_err(|e| FolderRepositoryError::StorageError(e.to_string()))?;
|
||||
}
|
||||
|
||||
@@ -51,7 +52,10 @@ impl FolderFsRepository {
|
||||
|
||||
// Verify the folder exists
|
||||
if !folder_path_buf.exists() {
|
||||
return Err(FolderRepositoryError::NotFound(format!("Folder not found: {}", folder_id)));
|
||||
return Err(FolderRepositoryError::NotFound(format!(
|
||||
"Folder not found: {}",
|
||||
folder_id
|
||||
)));
|
||||
}
|
||||
|
||||
// Create directory in the trash
|
||||
@@ -60,16 +64,23 @@ impl FolderFsRepository {
|
||||
// Physically move the folder to the trash
|
||||
match fs::rename(&folder_path_buf, &trash_folder_path).await {
|
||||
Ok(_) => {
|
||||
debug!("Folder moved to trash: {} -> {}", folder_path_buf.display(), trash_folder_path.display());
|
||||
debug!(
|
||||
"Folder moved to trash: {} -> {}",
|
||||
folder_path_buf.display(),
|
||||
trash_folder_path.display()
|
||||
);
|
||||
|
||||
// Update the mapping to the new path in the trash
|
||||
if let Err(e) = self.update_mapped_folder_path(folder_id, &trash_folder_path).await {
|
||||
if let Err(e) = self
|
||||
.update_mapped_folder_path(folder_id, &trash_folder_path)
|
||||
.await
|
||||
{
|
||||
error!("Error updating folder mapping in trash: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error moving folder to trash: {}", e);
|
||||
Err(FolderRepositoryError::StorageError(e.to_string()))
|
||||
@@ -78,7 +89,11 @@ impl FolderFsRepository {
|
||||
}
|
||||
|
||||
/// Restores a folder from the trash to its original location
|
||||
pub(crate) async fn _trash_restore_from_trash(&self, folder_id: &str, original_path: &str) -> FolderRepositoryResult<()> {
|
||||
pub(crate) async fn _trash_restore_from_trash(
|
||||
&self,
|
||||
folder_id: &str,
|
||||
original_path: &str,
|
||||
) -> FolderRepositoryResult<()> {
|
||||
debug!("Restoring folder {} to {}", folder_id, original_path);
|
||||
|
||||
// Get the current path in the trash
|
||||
@@ -95,9 +110,9 @@ impl FolderFsRepository {
|
||||
|
||||
// Ensure the destination parent directory exists
|
||||
if let Some(parent) = original_path_buf.parent()
|
||||
&& !parent.exists() {
|
||||
fs::create_dir_all(parent).await
|
||||
.map_err(|e| {
|
||||
&& !parent.exists()
|
||||
{
|
||||
fs::create_dir_all(parent).await.map_err(|e| {
|
||||
error!("Error creating parent directory for restoration: {}", e);
|
||||
FolderRepositoryError::StorageError(e.to_string())
|
||||
})?;
|
||||
@@ -106,16 +121,23 @@ impl FolderFsRepository {
|
||||
// Move the folder from the trash to its original location
|
||||
match fs::rename(¤t_path, &original_path_buf).await {
|
||||
Ok(_) => {
|
||||
debug!("Folder restored: {} -> {}", current_path.display(), original_path_buf.display());
|
||||
debug!(
|
||||
"Folder restored: {} -> {}",
|
||||
current_path.display(),
|
||||
original_path_buf.display()
|
||||
);
|
||||
|
||||
// Update the mapping to the original path
|
||||
if let Err(e) = self.update_mapped_folder_path(folder_id, &original_path_buf).await {
|
||||
if let Err(e) = self
|
||||
.update_mapped_folder_path(folder_id, &original_path_buf)
|
||||
.await
|
||||
{
|
||||
error!("Error updating restored folder mapping: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error restoring folder: {}", e);
|
||||
Err(FolderRepositoryError::StorageError(e.to_string()))
|
||||
@@ -124,7 +146,10 @@ impl FolderFsRepository {
|
||||
}
|
||||
|
||||
/// Permanently deletes a folder (used by the trash)
|
||||
pub(crate) async fn _trash_delete_folder_permanently(&self, folder_id: &str) -> FolderRepositoryResult<()> {
|
||||
pub(crate) async fn _trash_delete_folder_permanently(
|
||||
&self,
|
||||
folder_id: &str,
|
||||
) -> FolderRepositoryResult<()> {
|
||||
debug!("Permanently deleting folder: {}", folder_id);
|
||||
|
||||
// Similar to delete_folder but without additional validations
|
||||
@@ -141,7 +166,7 @@ impl FolderFsRepository {
|
||||
match fs::remove_dir_all(&folder_path).await {
|
||||
Ok(_) => {
|
||||
debug!("Folder permanently deleted: {}", folder_path.display());
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error permanently deleting folder: {}", e);
|
||||
// Don't report error if the folder no longer exists
|
||||
|
||||
@@ -3,19 +3,19 @@ pub mod parallel_file_processor;
|
||||
pub mod repository_errors;
|
||||
|
||||
// Repositorios CQRS (Read/Write) + composite
|
||||
pub mod composite_file_repository;
|
||||
pub mod file_fs_read_repository;
|
||||
pub mod file_fs_write_repository;
|
||||
pub mod composite_file_repository;
|
||||
|
||||
pub mod trash_fs_repository;
|
||||
pub mod folder_fs_repository_trash;
|
||||
pub mod share_fs_repository;
|
||||
pub mod trash_fs_repository;
|
||||
|
||||
// Repositorios PostgreSQL
|
||||
pub mod pg;
|
||||
|
||||
// Re-exportar para facilitar acceso
|
||||
pub use composite_file_repository::CompositeFileRepository;
|
||||
pub use file_fs_read_repository::FileFsReadRepository;
|
||||
pub use file_fs_write_repository::FileFsWriteRepository;
|
||||
pub use composite_file_repository::CompositeFileRepository;
|
||||
pub use pg::{UserPgRepository, SessionPgRepository};
|
||||
pub use pg::{SessionPgRepository, UserPgRepository};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::future::join_all;
|
||||
use std::io::{self, SeekFrom};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::io::{self, SeekFrom};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
use tokio::task;
|
||||
use tokio::sync::{Semaphore, Mutex};
|
||||
use futures::future::join_all;
|
||||
use tracing::{info, debug, error};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::infrastructure::repositories::repository_errors::FileRepositoryError;
|
||||
@@ -119,26 +119,31 @@ impl ParallelFileProcessor {
|
||||
/// Divides a file into chunks for parallel processing
|
||||
pub fn calculate_chunks(&self, file_size: u64) -> Vec<ChunkRange> {
|
||||
// Determine if the file needs parallel processing
|
||||
let needs_parallel = self.config.resources.needs_parallel_processing(
|
||||
file_size, &self.config.concurrency
|
||||
);
|
||||
let needs_parallel = self
|
||||
.config
|
||||
.resources
|
||||
.needs_parallel_processing(file_size, &self.config.concurrency);
|
||||
|
||||
if !needs_parallel {
|
||||
// For small files, use a single chunk
|
||||
return vec![ChunkRange {
|
||||
index: 0,
|
||||
start: 0,
|
||||
size: file_size as usize
|
||||
size: file_size as usize,
|
||||
}];
|
||||
}
|
||||
|
||||
// Calculate optimal number of chunks
|
||||
let chunk_count = self.config.resources.calculate_optimal_chunks(
|
||||
file_size, &self.config.concurrency
|
||||
);
|
||||
let chunk_count = self
|
||||
.config
|
||||
.resources
|
||||
.calculate_optimal_chunks(file_size, &self.config.concurrency);
|
||||
|
||||
// Calculate size of each chunk
|
||||
let chunk_size = self.config.resources.calculate_chunk_size(file_size, chunk_count);
|
||||
let chunk_size = self
|
||||
.config
|
||||
.resources
|
||||
.calculate_chunk_size(file_size, chunk_count);
|
||||
|
||||
// Create chunk ranges
|
||||
let mut chunks = Vec::with_capacity(chunk_count);
|
||||
@@ -161,28 +166,36 @@ impl ParallelFileProcessor {
|
||||
start += current_chunk_size as u64;
|
||||
}
|
||||
|
||||
debug!("File size: {} bytes, divided into {} chunks of ~{} bytes each",
|
||||
file_size, chunks.len(), chunk_size);
|
||||
debug!(
|
||||
"File size: {} bytes, divided into {} chunks of ~{} bytes each",
|
||||
file_size,
|
||||
chunks.len(),
|
||||
chunk_size
|
||||
);
|
||||
|
||||
chunks
|
||||
}
|
||||
|
||||
/// Reads a file in parallel and returns the complete content
|
||||
/// Optimized implementation using BytesMut to reduce memory copies
|
||||
pub async fn read_file_parallel(&self, file_path: &PathBuf) -> Result<Vec<u8>, FileRepositoryError> {
|
||||
pub async fn read_file_parallel(
|
||||
&self,
|
||||
file_path: &PathBuf,
|
||||
) -> Result<Vec<u8>, FileRepositoryError> {
|
||||
// Get file size
|
||||
let metadata = tokio::fs::metadata(file_path).await
|
||||
let metadata = tokio::fs::metadata(file_path)
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
let file_size = metadata.len();
|
||||
|
||||
// Check if the file is too large for memory
|
||||
if !self.config.resources.can_load_in_memory(file_size) {
|
||||
return Err(FileRepositoryError::Other(
|
||||
format!("File too large to load in memory: {} MB (max: {} MB)",
|
||||
return Err(FileRepositoryError::Other(format!(
|
||||
"File too large to load in memory: {} MB (max: {} MB)",
|
||||
file_size / (1024 * 1024),
|
||||
self.config.resources.max_in_memory_file_size_mb)
|
||||
));
|
||||
self.config.resources.max_in_memory_file_size_mb
|
||||
)));
|
||||
}
|
||||
|
||||
// Calculate chunks
|
||||
@@ -190,7 +203,10 @@ impl ParallelFileProcessor {
|
||||
|
||||
if chunks.len() == 1 {
|
||||
// For a single chunk, use simple reading with buffer pool if available
|
||||
info!("Reading file with size {}MB as a single chunk", file_size / (1024 * 1024));
|
||||
info!(
|
||||
"Reading file with size {}MB as a single chunk",
|
||||
file_size / (1024 * 1024)
|
||||
);
|
||||
|
||||
if let Some(pool) = &self.buffer_pool {
|
||||
// Use buffer from the pool for efficient reading
|
||||
@@ -199,18 +215,25 @@ impl ParallelFileProcessor {
|
||||
|
||||
// If the buffer is too small, revert to standard implementation
|
||||
if buffer.capacity() < file_size as usize {
|
||||
debug!("Buffer from pool too small ({}), using standard read", buffer.capacity());
|
||||
let content = tokio::fs::read(file_path).await
|
||||
debug!(
|
||||
"Buffer from pool too small ({}), using standard read",
|
||||
buffer.capacity()
|
||||
);
|
||||
let content = tokio::fs::read(file_path)
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
return Ok(content);
|
||||
}
|
||||
|
||||
// Use memory buffer from the pool
|
||||
let mut file = File::open(file_path).await
|
||||
let mut file = File::open(file_path)
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
let read_size = file.read(buffer.as_mut_slice()).await
|
||||
let read_size = file
|
||||
.read(buffer.as_mut_slice())
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
buffer.set_used(read_size);
|
||||
@@ -220,7 +243,8 @@ impl ParallelFileProcessor {
|
||||
return Ok(content);
|
||||
} else {
|
||||
// Standard implementation without pool
|
||||
let content = tokio::fs::read(file_path).await
|
||||
let content = tokio::fs::read(file_path)
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
return Ok(content);
|
||||
@@ -228,8 +252,11 @@ impl ParallelFileProcessor {
|
||||
}
|
||||
|
||||
// For multiple chunks, use parallel reading
|
||||
info!("Reading file with size {}MB in {} parallel chunks using BytesMut",
|
||||
file_size / (1024 * 1024), chunks.len());
|
||||
info!(
|
||||
"Reading file with size {}MB in {} parallel chunks using BytesMut",
|
||||
file_size / (1024 * 1024),
|
||||
chunks.len()
|
||||
);
|
||||
|
||||
// Create final result buffer (pre-allocated)
|
||||
let mut result = BytesMut::with_capacity(file_size as usize);
|
||||
@@ -240,8 +267,11 @@ impl ParallelFileProcessor {
|
||||
let mut tasks = Vec::with_capacity(chunks.len());
|
||||
|
||||
// Open file once and share it
|
||||
let file = Arc::new(File::open(file_path).await
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?);
|
||||
let file = Arc::new(
|
||||
File::open(file_path)
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?,
|
||||
);
|
||||
|
||||
// Reference to BytesMut pool
|
||||
let bytes_pool = self.bytes_pool.clone();
|
||||
@@ -273,12 +303,17 @@ impl ParallelFileProcessor {
|
||||
|
||||
// Position and read directly into the BytesMut
|
||||
file_handle.seek(SeekFrom::Start(chunk.start)).await?;
|
||||
let bytes_read = file_handle.read_exact(&mut chunk_buffer[..chunk.size]).await?;
|
||||
let bytes_read = file_handle
|
||||
.read_exact(&mut chunk_buffer[..chunk.size])
|
||||
.await?;
|
||||
|
||||
if bytes_read != chunk.size {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
format!("Expected to read {} bytes but got {}", chunk.size, bytes_read)
|
||||
format!(
|
||||
"Expected to read {} bytes but got {}",
|
||||
chunk.size, bytes_read
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -294,8 +329,10 @@ impl ParallelFileProcessor {
|
||||
bytes_pool_clone.return_buffer(chunk_buffer).await;
|
||||
|
||||
// Log progress
|
||||
debug!("Chunk {} processed: {} bytes from offset {}",
|
||||
chunk.index, chunk.size, chunk.start);
|
||||
debug!(
|
||||
"Chunk {} processed: {} bytes from offset {}",
|
||||
chunk.index, chunk.size, chunk.start
|
||||
);
|
||||
|
||||
Ok::<_, io::Error>(())
|
||||
});
|
||||
@@ -309,11 +346,11 @@ impl ParallelFileProcessor {
|
||||
// Check for errors
|
||||
for (i, task_result) in results.into_iter().enumerate() {
|
||||
match task_result {
|
||||
Ok(Ok(())) => {},
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => {
|
||||
error!("Error in chunk {}: {}", i, e);
|
||||
return Err(FileRepositoryError::StorageError(e.to_string()));
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Task error in chunk {}: {}", i, e);
|
||||
return Err(FileRepositoryError::Other(format!("Task error: {}", e)));
|
||||
@@ -325,7 +362,10 @@ impl ParallelFileProcessor {
|
||||
let result_buffer = result_mutex.lock().await;
|
||||
let result_vec = result_buffer.to_vec();
|
||||
|
||||
info!("Successfully read file of {}MB in parallel with optimized BytesMut", file_size / (1024 * 1024));
|
||||
info!(
|
||||
"Successfully read file of {}MB in parallel with optimized BytesMut",
|
||||
file_size / (1024 * 1024)
|
||||
);
|
||||
Ok(result_vec)
|
||||
}
|
||||
|
||||
@@ -334,7 +374,7 @@ impl ParallelFileProcessor {
|
||||
pub async fn write_file_parallel(
|
||||
&self,
|
||||
file_path: &PathBuf,
|
||||
content: &[u8]
|
||||
content: &[u8],
|
||||
) -> Result<(), FileRepositoryError> {
|
||||
let file_size = content.len() as u64;
|
||||
|
||||
@@ -343,21 +383,29 @@ impl ParallelFileProcessor {
|
||||
|
||||
if chunks.len() == 1 {
|
||||
// For a single chunk, use simple writing
|
||||
info!("Writing file with size {}MB as a single chunk", file_size / (1024 * 1024));
|
||||
info!(
|
||||
"Writing file with size {}MB as a single chunk",
|
||||
file_size / (1024 * 1024)
|
||||
);
|
||||
|
||||
// Standard implementation (buffer pooling offers no advantages for simple writing)
|
||||
tokio::fs::write(file_path, content).await
|
||||
tokio::fs::write(file_path, content)
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// For multiple chunks, use parallel writing
|
||||
info!("Writing file with size {}MB in {} parallel chunks using Bytes",
|
||||
file_size / (1024 * 1024), chunks.len());
|
||||
info!(
|
||||
"Writing file with size {}MB in {} parallel chunks using Bytes",
|
||||
file_size / (1024 * 1024),
|
||||
chunks.len()
|
||||
);
|
||||
|
||||
// Create file (we don't use Mutex to reduce contention)
|
||||
let file = File::create(file_path).await
|
||||
let file = File::create(file_path)
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
// Convert content to Bytes (single copy step)
|
||||
@@ -368,7 +416,9 @@ impl ParallelFileProcessor {
|
||||
|
||||
// Process chunks in parallel
|
||||
for chunk in chunks {
|
||||
let file_clone = file.try_clone().await
|
||||
let file_clone = file
|
||||
.try_clone()
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
let semaphore_clone = self.concurrency_limiter.clone();
|
||||
|
||||
@@ -388,8 +438,10 @@ impl ParallelFileProcessor {
|
||||
file_handle.write_all(&chunk_data).await?;
|
||||
|
||||
// Log progress
|
||||
debug!("Chunk {} written: {} bytes at offset {}",
|
||||
chunk.index, chunk.size, chunk.start);
|
||||
debug!(
|
||||
"Chunk {} written: {} bytes at offset {}",
|
||||
chunk.index, chunk.size, chunk.start
|
||||
);
|
||||
|
||||
Ok::<_, io::Error>(())
|
||||
});
|
||||
@@ -403,11 +455,11 @@ impl ParallelFileProcessor {
|
||||
// Check for errors
|
||||
for (i, task_result) in results.into_iter().enumerate() {
|
||||
match task_result {
|
||||
Ok(Ok(())) => {},
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => {
|
||||
error!("Error in chunk {}: {}", i, e);
|
||||
return Err(FileRepositoryError::StorageError(e.to_string()));
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Task error in chunk {}: {}", i, e);
|
||||
return Err(FileRepositoryError::Other(format!("Task error: {}", e)));
|
||||
@@ -417,9 +469,15 @@ impl ParallelFileProcessor {
|
||||
|
||||
// Ensure everything has been written correctly
|
||||
let mut file_handle = file;
|
||||
file_handle.flush().await.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
file_handle
|
||||
.flush()
|
||||
.await
|
||||
.map_err(|e| FileRepositoryError::StorageError(e.to_string()))?;
|
||||
|
||||
info!("Successfully wrote file of {}MB in parallel with optimized Bytes", file_size / (1024 * 1024));
|
||||
info!(
|
||||
"Successfully wrote file of {}MB in parallel with optimized Bytes",
|
||||
file_size / (1024 * 1024)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -451,7 +509,10 @@ mod tests {
|
||||
}
|
||||
|
||||
// Write file in parallel
|
||||
processor.write_file_parallel(&file_path, &test_data).await.unwrap();
|
||||
processor
|
||||
.write_file_parallel(&file_path, &test_data)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Read file in parallel
|
||||
let read_data = processor.read_file_parallel(&file_path).await.unwrap();
|
||||
|
||||
@@ -3,9 +3,11 @@ use chrono::Utc;
|
||||
use sqlx::{PgPool, Row, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::domain::entities::contact::AddressBook;
|
||||
use crate::domain::repositories::address_book_repository::{AddressBookRepository, AddressBookRepositoryResult};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::contact::AddressBook;
|
||||
use crate::domain::repositories::address_book_repository::{
|
||||
AddressBookRepository, AddressBookRepositoryResult,
|
||||
};
|
||||
|
||||
pub struct AddressBookPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
@@ -19,7 +21,10 @@ impl AddressBookPgRepository {
|
||||
|
||||
#[async_trait]
|
||||
impl AddressBookRepository for AddressBookPgRepository {
|
||||
async fn create_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook> {
|
||||
async fn create_address_book(
|
||||
&self,
|
||||
address_book: AddressBook,
|
||||
) -> AddressBookRepositoryResult<AddressBook> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.address_books (id, name, owner_id, description, color, is_public, created_at, updated_at)
|
||||
@@ -51,7 +56,10 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
))
|
||||
}
|
||||
|
||||
async fn update_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook> {
|
||||
async fn update_address_book(
|
||||
&self,
|
||||
address_book: AddressBook,
|
||||
) -> AddressBookRepositoryResult<AddressBook> {
|
||||
let now = Utc::now();
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
@@ -59,7 +67,7 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
SET name = $1, description = $2, color = $3, is_public = $4, updated_at = $5
|
||||
WHERE id = $6
|
||||
RETURNING id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(address_book.name())
|
||||
.bind(address_book.description())
|
||||
@@ -69,7 +77,9 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
.bind(address_book.id())
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to update address book: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to update address book: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
@@ -88,30 +98,38 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
r#"
|
||||
DELETE FROM carddav.address_books
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to delete address book: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to delete address book: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_address_book_by_id(&self, id: &Uuid) -> AddressBookRepositoryResult<Option<AddressBook>> {
|
||||
async fn get_address_book_by_id(
|
||||
&self,
|
||||
id: &Uuid,
|
||||
) -> AddressBookRepositoryResult<Option<AddressBook>> {
|
||||
let maybe_row = sqlx::query(
|
||||
r#"
|
||||
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
FROM carddav.address_books
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get address book by id: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get address book by id: {}", e))
|
||||
})?;
|
||||
|
||||
let result = maybe_row.map(|row| AddressBook::from_raw(
|
||||
let result = maybe_row.map(|row| {
|
||||
AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
@@ -120,27 +138,35 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
));
|
||||
)
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn get_address_books_by_owner(&self, owner_id: &str) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
||||
async fn get_address_books_by_owner(
|
||||
&self,
|
||||
owner_id: &str,
|
||||
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
FROM carddav.address_books
|
||||
WHERE owner_id = $1
|
||||
ORDER BY name
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(owner_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get address books by owner: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get address books by owner: {}", e))
|
||||
})?;
|
||||
|
||||
let result = rows.into_iter()
|
||||
.map(|row| AddressBook::from_raw(
|
||||
let result = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
@@ -149,13 +175,17 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
))
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn get_shared_address_books(&self, user_id: &str) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
||||
async fn get_shared_address_books(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT a.id, a.name, a.owner_id, a.description, a.color, a.is_public, a.created_at, a.updated_at
|
||||
@@ -170,8 +200,10 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get shared address books: {}", e)))?;
|
||||
|
||||
let result = rows.into_iter()
|
||||
.map(|row| AddressBook::from_raw(
|
||||
let result = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
@@ -180,7 +212,8 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
))
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
@@ -193,14 +226,18 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
FROM carddav.address_books
|
||||
WHERE is_public = true
|
||||
ORDER BY name
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get public address books: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get public address books: {}", e))
|
||||
})?;
|
||||
|
||||
let result = rows.into_iter()
|
||||
.map(|row| AddressBook::from_raw(
|
||||
let result = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
@@ -209,19 +246,25 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
))
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn share_address_book(&self, address_book_id: &Uuid, user_id: &str, can_write: bool) -> AddressBookRepositoryResult<()> {
|
||||
async fn share_address_book(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
user_id: &str,
|
||||
can_write: bool,
|
||||
) -> AddressBookRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.address_book_shares (address_book_id, user_id, can_write)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (address_book_id, user_id) DO UPDATE SET can_write = $3
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.bind(user_id)
|
||||
@@ -233,37 +276,49 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn unshare_address_book(&self, address_book_id: &Uuid, user_id: &str) -> AddressBookRepositoryResult<()> {
|
||||
async fn unshare_address_book(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
user_id: &str,
|
||||
) -> AddressBookRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM carddav.address_book_shares
|
||||
WHERE address_book_id = $1 AND user_id = $2
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.bind(user_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to unshare address book: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to unshare address book: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_address_book_shares(&self, address_book_id: &Uuid) -> AddressBookRepositoryResult<Vec<(String, bool)>> {
|
||||
async fn get_address_book_shares(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
) -> AddressBookRepositoryResult<Vec<(String, bool)>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT user_id, can_write
|
||||
FROM carddav.address_book_shares
|
||||
WHERE address_book_id = $1
|
||||
ORDER BY user_id
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get address book shares: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get address book shares: {}", e))
|
||||
})?;
|
||||
|
||||
let result = rows.into_iter()
|
||||
let result = rows
|
||||
.into_iter()
|
||||
.map(|row| (row.get("user_id"), row.get("can_write")))
|
||||
.collect();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user