adding card dav and cald dav

This commit is contained in:
DioCrafts
2025-04-13 01:04:04 +02:00
parent f2ecbc1a39
commit 52d8250d51
52 changed files with 8056 additions and 536 deletions
+813
View File
@@ -0,0 +1,813 @@
/**
* 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 uuid::Uuid;
use crate::application::adapters::webdav_adapter::{WebDavAdapter, QualifiedName, PropFindType, PropFindRequest, Result, WebDavError};
use crate::application::dtos::calendar_dto::{CalendarDto, CalendarEventDto};
/// CalDAV report type
#[derive(Debug, PartialEq)]
pub enum CalDavReportType {
/// Calendar-query report
CalendarQuery {
time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
props: Vec<QualifiedName>,
},
/// Calendar-multiget report
CalendarMultiget {
hrefs: Vec<String>,
props: Vec<QualifiedName>,
},
/// Sync-collection report
SyncCollection {
sync_token: String,
props: Vec<QualifiedName>,
}
}
/// CalDAV adapter for converting between XML and domain objects
pub struct CalDavAdapter;
impl CalDavAdapter {
/// Parse a REPORT XML request for CalDAV
pub fn parse_report<R: Read>(reader: R) -> Result<CalDavReportType> {
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
xml_reader.config_mut().trim_text(true);
let mut buffer = Vec::new();
let mut in_calendar_query = false;
let mut in_calendar_multiget = false;
let mut in_sync_collection = false;
let mut in_prop = false;
let mut in_filter = false;
let mut in_time_range = false;
let mut start_time: Option<DateTime<Utc>> = None;
let mut end_time: Option<DateTime<Utc>> = None;
let mut props = Vec::new();
let mut hrefs = Vec::new();
let mut sync_token = String::new();
loop {
match xml_reader.read_event_into(&mut buffer) {
Ok(Event::Start(ref e)) => {
let name = e.name();
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 == "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") => {
in_time_range = true;
// 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_value = attr.unescape_value().unwrap_or_default();
if attr_name == "start" {
// Parse ISO date format with Z for UTC
start_time = DateTime::parse_from_rfc3339(&attr_value)
.ok()
.map(|dt| dt.with_timezone(&Utc));
} else if attr_name == "end" {
end_time = DateTime::parse_from_rfc3339(&attr_value)
.ok()
.map(|dt| dt.with_timezone(&Utc));
}
}
}
},
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.unescape().unwrap_or_default();
// Check if we're in sync-token element
if in_sync_collection && !in_prop && !in_filter {
sync_token = text.to_string();
}
// Check if we're in href element
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("");
match name_str {
s if s == "calendar-query" || s.ends_with(":calendar-query") => in_calendar_query = false,
s if s == "calendar-multiget" || s.ends_with(":calendar-multiget") => in_calendar_multiget = false,
s if s == "sync-collection" || s.ends_with(":sync-collection") => in_sync_collection = false,
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") => in_time_range = false,
_ => ()
}
},
Ok(Event::Empty(ref e)) => {
let name = e.name();
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
if in_prop {
// Add empty property element 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));
} else if name_str == "time-range" || name_str.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_value = attr.unescape_value().unwrap_or_default();
if attr_name == "start" {
// Parse ISO date format with Z for UTC
start_time = DateTime::parse_from_rfc3339(&attr_value)
.ok()
.map(|dt| dt.with_timezone(&Utc));
} else if attr_name == "end" {
end_time = DateTime::parse_from_rfc3339(&attr_value)
.ok()
.map(|dt| dt.with_timezone(&Utc));
}
}
}
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(WebDavError::XmlError(e)),
_ => (),
}
buffer.clear();
}
// Create the appropriate report type based on what we parsed
let report_type = if in_calendar_query {
// If both start and end time are present, create a time range
let time_range = if let (Some(start), Some(end)) = (start_time, end_time) {
Some((start, end))
} else {
None
};
CalDavReportType::CalendarQuery {
time_range,
props,
}
} else if in_calendar_multiget {
CalDavReportType::CalendarMultiget {
hrefs,
props,
}
} else if in_sync_collection {
CalDavReportType::SyncCollection {
sync_token,
props,
}
} else {
// Default to empty calendar query
CalDavReportType::CalendarQuery {
time_range: None,
props,
}
};
Ok(report_type)
}
/// Generate a PROPFIND response for calendars
pub fn generate_calendars_propfind_response<W: Write>(
writer: W,
calendars: &[CalendarDto],
request: &PropFindRequest,
base_href: &str,
) -> Result<()> {
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:"),
("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))?;
}
// End multistatus
xml_writer.write_event(Event::End(BytesEnd::new("D:multistatus")))?;
Ok(())
}
/// Write calendar properties as a response
fn write_calendar_response<W: Write>(
xml_writer: &mut Writer<W>,
calendar: &CalendarDto,
request: &PropFindRequest,
href: &str,
) -> Result<()> {
// Start response element
xml_writer.write_event(Event::Start(BytesStart::new("D:response")))?;
// Write href
xml_writer.write_event(Event::Start(BytesStart::new("D:href")))?;
xml_writer.write_event(Event::Text(BytesText::new(href)))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:href")))?;
// Write propstat
xml_writer.write_event(Event::Start(BytesStart::new("D:propstat")))?;
// Start prop
xml_writer.write_event(Event::Start(BytesStart::new("D:prop")))?;
// Write properties based on request type
match &request.prop_find_type {
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)?;
}
}
// End prop
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
// Write status
xml_writer.write_event(Event::Start(BytesStart::new("D:status")))?;
xml_writer.write_event(Event::Text(BytesText::new("HTTP/1.1 200 OK")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:status")))?;
// End propstat
xml_writer.write_event(Event::End(BytesEnd::new("D:propstat")))?;
// End response
xml_writer.write_event(Event::End(BytesEnd::new("D:response")))?;
Ok(())
}
/// Write standard calendar properties
fn write_calendar_standard_props<W: Write>(
xml_writer: &mut Writer<W>,
calendar: &CalendarDto,
) -> Result<()> {
// Common WebDAV properties
// Resource type (collection + calendar)
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::Empty(BytesStart::new("C:calendar")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:resourcetype")))?;
// Display name
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")))?;
// 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::End(BytesEnd::new("D:getlastmodified")))?;
// ETag
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::End(BytesEnd::new("D:getetag")))?;
// 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::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")))?;
// Calendar timezone (empty for UTC)
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-timezone")))?;
// Calendar color
if let Some(color) = &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")))?;
}
// Support calendar-access (RFC4791)
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: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
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")))?;
// Calendar description if present
if let Some(desc) = &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")))?;
}
// Custom properties
for (name, value) in &calendar.custom_properties {
// Skip properties that start with _ - they're internal
if !name.starts_with('_') {
xml_writer.write_event(Event::Start(BytesStart::new(&format!("CS:{}", name))))?;
xml_writer.write_event(Event::Text(BytesText::new(value)))?;
xml_writer.write_event(Event::End(BytesEnd::new(&format!("CS:{}", name))))?;
}
}
Ok(())
}
/// Write calendar property names
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")))?;
xml_writer.write_event(Event::Empty(BytesStart::new("D:getlastmodified")))?;
xml_writer.write_event(Event::Empty(BytesStart::new("D:getetag")))?;
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: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("C:calendar-description")))?;
Ok(())
}
/// Write requested calendar properties
fn write_calendar_requested_props<W: Write>(
xml_writer: &mut Writer<W>,
calendar: &CalendarDto,
props: &[QualifiedName],
) -> Result<()> {
for prop in props {
match (prop.namespace.as_str(), prop.name.as_str()) {
// DAV namespace properties
("DAV:", "resourcetype") => {
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::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::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::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::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: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
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")))?;
},
// 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")))?;
},
("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::Text(BytesText::new(desc)))?;
xml_writer.write_event(Event::End(BytesEnd::new("C:calendar-description")))?;
} else {
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::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")))?;
}
},
// Custom properties from the calendar
_ => {
// Check if it's a custom property
if let Some(value) = calendar.custom_properties.get(&prop.name) {
let prop_name = if prop.namespace == "http://calendarserver.org/ns/" {
format!("CS:{}", prop.name)
} else if prop.namespace == "urn:ietf:params:xml:ns:caldav" {
format!("C:{}", prop.name)
} else if prop.namespace == "DAV:" {
format!("D:{}", prop.name)
} else {
format!("{}:{}", prop.namespace, prop.name)
};
xml_writer.write_event(Event::Start(BytesStart::new(&prop_name)))?;
xml_writer.write_event(Event::Text(BytesText::new(value)))?;
xml_writer.write_event(Event::End(BytesEnd::new(&prop_name)))?;
} else {
// Property not found, write empty element
let prop_name = if prop.namespace == "http://calendarserver.org/ns/" {
format!("CS:{}", prop.name)
} else if prop.namespace == "urn:ietf:params:xml:ns:caldav" {
format!("C:{}", prop.name)
} else if prop.namespace == "DAV:" {
format!("D:{}", prop.name)
} else {
format!("{}:{}", prop.namespace, prop.name)
};
xml_writer.write_event(Event::Empty(BytesStart::new(&prop_name)))?;
}
}
}
}
Ok(())
}
/// Generate a response for calendar events
pub fn generate_calendar_events_response<W: Write>(
writer: W,
events: &[CalendarEventDto],
request: &CalDavReportType,
base_href: &str,
) -> Result<()> {
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:"),
("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 {
CalDavReportType::CalendarQuery { props, .. } => props.clone(),
CalDavReportType::CalendarMultiget { props, .. } => props.clone(),
CalDavReportType::SyncCollection { props, .. } => props.clone(),
};
// Add responses for events
for event in events {
// Create the event href based on its UID
let href = format!("{}{}.ics", base_href, event.ical_uid);
// Write event response
Self::write_event_response(&mut xml_writer, event, &props, &href)?;
}
// End multistatus
xml_writer.write_event(Event::End(BytesEnd::new("D:multistatus")))?;
Ok(())
}
/// Write event properties as a response
fn write_event_response<W: Write>(
xml_writer: &mut Writer<W>,
event: &CalendarEventDto,
props: &[QualifiedName],
href: &str,
) -> Result<()> {
// Start response element
xml_writer.write_event(Event::Start(BytesStart::new("D:response")))?;
// Write href
xml_writer.write_event(Event::Start(BytesStart::new("D:href")))?;
xml_writer.write_event(Event::Text(BytesText::new(href)))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:href")))?;
// Write propstat
xml_writer.write_event(Event::Start(BytesStart::new("D:propstat")))?;
// Start prop
xml_writer.write_event(Event::Start(BytesStart::new("D:prop")))?;
// If no specific props requested, return all common ones
if props.is_empty() {
Self::write_event_standard_props(xml_writer, event)?;
} else {
// Write specifically requested properties
Self::write_event_requested_props(xml_writer, event, props)?;
}
// End prop
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
// Write status
xml_writer.write_event(Event::Start(BytesStart::new("D:status")))?;
xml_writer.write_event(Event::Text(BytesText::new("HTTP/1.1 200 OK")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:status")))?;
// End propstat
xml_writer.write_event(Event::End(BytesEnd::new("D:propstat")))?;
// End response
xml_writer.write_event(Event::End(BytesEnd::new("D:response")))?;
Ok(())
}
/// Write standard event properties
fn write_event_standard_props<W: Write>(
xml_writer: &mut Writer<W>,
event: &CalendarEventDto,
) -> Result<()> {
// Common WebDAV properties
// Resource type (empty for non-collection)
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
// ETag based on updated_at timestamp
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::End(BytesEnd::new("D:getetag")))?;
// 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::End(BytesEnd::new("D:getcontenttype")))?;
// Last modified
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::End(BytesEnd::new("D:getlastmodified")))?;
// CalDAV specific properties
// Calendar data (iCalendar format)
xml_writer.write_event(Event::Start(BytesStart::new("C:calendar-data")))?;
// In a full implementation, we would generate a complete iCalendar component here
// For now, we'll just provide a basic example
let ical_data = format!(
"BEGIN:VCALENDAR\r\n\
VERSION:2.0\r\n\
PRODID:-//OxiCloud//NONSGML Calendar//EN\r\n\
BEGIN:VEVENT\r\n\
UID:{}\r\n\
SUMMARY:{}\r\n\
DTSTART:{}\r\n\
DTEND:{}\r\n\
{}\
DTSTAMP:{}\r\n\
END:VEVENT\r\n\
END:VCALENDAR\r\n",
event.ical_uid,
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.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")))?;
Ok(())
}
/// Write requested event properties
fn write_event_requested_props<W: Write>(
xml_writer: &mut Writer<W>,
event: &CalendarEventDto,
props: &[QualifiedName],
) -> Result<()> {
for prop in props {
match (prop.namespace.as_str(), prop.name.as_str()) {
// 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::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::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::End(BytesEnd::new("D:getlastmodified")))?;
},
// CalDAV namespace properties
("urn:ietf:params:xml:ns:caldav", "calendar-data") => {
xml_writer.write_event(Event::Start(BytesStart::new("C:calendar-data")))?;
// In a full implementation, we would generate a complete iCalendar component here
// For now, we'll just provide a basic example
let ical_data = format!(
"BEGIN:VCALENDAR\r\n\
VERSION:2.0\r\n\
PRODID:-//OxiCloud//NONSGML Calendar//EN\r\n\
BEGIN:VEVENT\r\n\
UID:{}\r\n\
SUMMARY:{}\r\n\
DTSTART:{}\r\n\
DTEND:{}\r\n\
{}\
DTSTAMP:{}\r\n\
END:VEVENT\r\n\
END:VCALENDAR\r\n",
event.ical_uid,
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.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
_ => {
// Write empty element
let prop_name = if prop.namespace == "http://calendarserver.org/ns/" {
format!("CS:{}", prop.name)
} else if prop.namespace == "urn:ietf:params:xml:ns:caldav" {
format!("C:{}", prop.name)
} else if prop.namespace == "DAV:" {
format!("D:{}", prop.name)
} else {
format!("{}:{}", prop.namespace, prop.name)
};
xml_writer.write_event(Event::Empty(BytesStart::new(&prop_name)))?;
}
}
}
Ok(())
}
/// Parse a MKCALENDAR XML request
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);
let mut buffer = Vec::new();
let mut in_mkcalendar = false;
let mut in_set = false;
let mut in_prop = false;
let mut in_displayname = false;
let mut in_description = false;
let mut in_calendar_color = false;
let mut displayname = String::new();
let mut description = None;
let mut color = None;
loop {
match xml_reader.read_event_into(&mut buffer) {
Ok(Event::Start(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 = 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,
_ => ()
}
},
Ok(Event::Text(e)) => {
let text = e.unescape().unwrap_or_default();
if in_displayname {
displayname = text.to_string();
} else if in_description {
description = Some(text.to_string());
} 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 == "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,
_ => ()
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(WebDavError::XmlError(e)),
_ => (),
}
buffer.clear();
}
// If no displayname specified, generate a default one based on UUID
if displayname.is_empty() {
displayname = format!("Calendar {}", Uuid::new_v4());
}
Ok((displayname, description, color))
}
}
+1
View File
@@ -1,3 +1,4 @@
//! Adapters module for translating between external protocols and internal models
pub mod webdav_adapter;
pub mod caldav_adapter;
+5 -5
View File
@@ -123,7 +123,7 @@ impl WebDavAdapter {
/// Parse a PROPFIND XML request
pub fn parse_propfind<R: Read>(reader: R) -> Result<PropFindRequest> {
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
xml_reader.trim_text(true);
xml_reader.config_mut().trim_text(true);
let mut buffer = Vec::new();
let mut in_propfind = false;
@@ -649,7 +649,7 @@ impl WebDavAdapter {
/// Parse a PROPPATCH XML request
pub fn parse_proppatch<R: Read>(reader: R) -> Result<(Vec<PropValue>, Vec<QualifiedName>)> {
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
xml_reader.trim_text(true);
xml_reader.config_mut().trim_text(true);
let mut buffer = Vec::new();
let mut in_propertyupdate = false;
@@ -849,7 +849,7 @@ impl WebDavAdapter {
/// Parse a LOCK XML request
pub fn parse_lockinfo<R: Read>(reader: R) -> Result<(LockScope, LockType, Option<String>)> {
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
xml_reader.trim_text(true);
xml_reader.config_mut().trim_text(true);
let mut buffer = Vec::new();
let mut in_lockinfo = false;
@@ -996,7 +996,7 @@ impl WebDavAdapter {
}
/// Helper method to extract namespace from tag name
fn extract_namespace(name: &str) -> String {
pub fn extract_namespace(name: &str) -> String {
if let Some(idx) = name.rfind(':') {
if idx > 0 {
return name[..idx].to_string();
@@ -1007,7 +1007,7 @@ impl WebDavAdapter {
}
/// Helper method to extract local name from tag name
fn extract_local_name(name: &str) -> String {
pub fn extract_local_name(name: &str) -> String {
if let Some(idx) = name.rfind(':') {
if idx > 0 && idx < name.len() - 1 {
return name[idx+1..].to_string();
+76
View File
@@ -0,0 +1,76 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::domain::entities::contact::AddressBook;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddressBookDto {
pub id: String,
pub name: String,
pub owner_id: String,
pub description: Option<String>,
pub color: Option<String>,
pub is_public: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Default for AddressBookDto {
fn default() -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
name: "Default Address Book".to_string(),
owner_id: "default".to_string(),
description: None,
color: None,
is_public: false,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
}
impl From<AddressBook> for AddressBookDto {
fn from(book: AddressBook) -> Self {
Self {
id: book.id.to_string(),
name: book.name,
owner_id: book.owner_id,
description: book.description,
color: book.color,
is_public: book.is_public,
created_at: book.created_at,
updated_at: book.updated_at,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateAddressBookDto {
pub name: String,
pub owner_id: String,
pub description: Option<String>,
pub color: Option<String>,
pub is_public: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateAddressBookDto {
pub name: Option<String>,
pub description: Option<String>,
pub color: Option<String>,
pub is_public: Option<bool>,
pub user_id: String, // Current user making the update
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShareAddressBookDto {
pub address_book_id: String,
pub user_id: String,
pub can_write: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnshareAddressBookDto {
pub address_book_id: String,
pub user_id: String,
}
+182
View File
@@ -0,0 +1,182 @@
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
use uuid::Uuid;
use std::collections::HashMap;
use crate::domain::entities::calendar::Calendar;
use crate::domain::entities::calendar_event::CalendarEvent;
/// DTO for calendar data transfer
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CalendarDto {
pub id: String,
pub name: String,
pub owner_id: String,
pub description: Option<String>,
pub color: Option<String>,
pub is_public: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub custom_properties: HashMap<String, String>,
}
impl Default for CalendarDto {
fn default() -> Self {
Self {
id: String::new(),
name: String::new(),
owner_id: String::new(),
description: None,
color: None,
is_public: false,
created_at: Utc::now(),
updated_at: Utc::now(),
custom_properties: HashMap::new(),
}
}
}
impl From<Calendar> for CalendarDto {
fn from(calendar: Calendar) -> Self {
Self {
id: calendar.id().to_string(),
name: calendar.name().to_string(),
owner_id: calendar.owner_id().to_string(),
description: calendar.description().map(|s| s.to_string()),
color: calendar.color().map(|s| s.to_string()),
is_public: false, // This needs to be set separately as it's not part of the domain entity
created_at: *calendar.created_at(),
updated_at: *calendar.updated_at(),
custom_properties: calendar.custom_properties().clone(),
}
}
}
/// DTO for calendar creation
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateCalendarDto {
pub name: String,
pub description: Option<String>,
pub color: Option<String>,
pub is_public: Option<bool>,
}
/// DTO for calendar update
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateCalendarDto {
pub name: Option<String>,
pub description: Option<String>,
pub color: Option<String>,
pub is_public: Option<bool>,
}
/// DTO for calendar sharing
#[derive(Debug, Serialize, Deserialize)]
pub struct CalendarShareDto {
pub calendar_id: String,
pub user_id: String,
pub access_level: String, // 'read', 'write', 'owner'
}
/// DTO for calendar event data transfer
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CalendarEventDto {
pub id: String,
pub calendar_id: String,
pub summary: String,
pub description: Option<String>,
pub location: Option<String>,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub all_day: bool,
pub rrule: Option<String>,
pub ical_uid: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Default for CalendarEventDto {
fn default() -> Self {
Self {
id: String::new(),
calendar_id: String::new(),
summary: String::new(),
description: None,
location: None,
start_time: Utc::now(),
end_time: Utc::now(),
all_day: false,
rrule: None,
ical_uid: String::new(),
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
}
impl From<CalendarEvent> for CalendarEventDto {
fn from(event: CalendarEvent) -> Self {
Self {
id: event.id().to_string(),
calendar_id: event.calendar_id().to_string(),
summary: event.summary().to_string(),
description: event.description().map(|s| s.to_string()),
location: event.location().map(|s| s.to_string()),
start_time: *event.start_time(),
end_time: *event.end_time(),
all_day: event.all_day(),
rrule: event.rrule().map(|s| s.to_string()),
ical_uid: event.ical_uid().to_string(),
created_at: *event.created_at(),
updated_at: *event.updated_at(),
}
}
}
/// DTO for calendar event creation using iCalendar data
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateEventICalDto {
pub calendar_id: String,
pub ical_data: String,
}
/// DTO for calendar event creation with structured data
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateEventDto {
pub calendar_id: String,
pub summary: String,
pub description: Option<String>,
pub location: Option<String>,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub all_day: Option<bool>,
pub rrule: Option<String>,
pub user_id: String, // Added for authorization
}
/// DTO for updating a calendar event
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateEventDto {
pub summary: Option<String>,
pub description: Option<String>,
pub location: Option<String>,
pub start_time: Option<DateTime<Utc>>,
pub end_time: Option<DateTime<Utc>>,
pub all_day: Option<bool>,
pub rrule: Option<String>,
pub user_id: String, // Added for authorization
}
/// DTO for querying events in a time range
#[derive(Debug, Serialize, Deserialize)]
pub struct EventQueryDto {
pub calendar_id: String,
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
/// DTO for pagination
#[derive(Debug, Serialize, Deserialize)]
pub struct PaginationDto {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
+223
View File
@@ -0,0 +1,223 @@
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 {
pub email: String,
pub r#type: String,
pub is_primary: bool,
}
impl From<Email> for EmailDto {
fn from(email: Email) -> Self {
Self {
email: email.email,
r#type: email.r#type,
is_primary: email.is_primary,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhoneDto {
pub number: String,
pub r#type: String,
pub is_primary: bool,
}
impl From<Phone> for PhoneDto {
fn from(phone: Phone) -> Self {
Self {
number: phone.number,
r#type: phone.r#type,
is_primary: phone.is_primary,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddressDto {
pub street: Option<String>,
pub city: Option<String>,
pub state: Option<String>,
pub postal_code: Option<String>,
pub country: Option<String>,
pub r#type: String,
pub is_primary: bool,
}
impl From<Address> for AddressDto {
fn from(address: Address) -> Self {
Self {
street: address.street,
city: address.city,
state: address.state,
postal_code: address.postal_code,
country: address.country,
r#type: address.r#type,
is_primary: address.is_primary,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactDto {
pub id: String,
pub address_book_id: String,
pub uid: String,
pub full_name: Option<String>,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub nickname: Option<String>,
pub email: Vec<EmailDto>,
pub phone: Vec<PhoneDto>,
pub address: Vec<AddressDto>,
pub organization: Option<String>,
pub title: Option<String>,
pub notes: Option<String>,
pub photo_url: Option<String>,
pub birthday: Option<NaiveDate>,
pub anniversary: Option<NaiveDate>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub etag: String,
}
impl Default for ContactDto {
fn default() -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
address_book_id: uuid::Uuid::new_v4().to_string(),
uid: format!("{}@oxicloud", uuid::Uuid::new_v4()),
full_name: None,
first_name: None,
last_name: None,
nickname: None,
email: Vec::new(),
phone: Vec::new(),
address: Vec::new(),
organization: None,
title: None,
notes: None,
photo_url: None,
birthday: None,
anniversary: None,
created_at: Utc::now(),
updated_at: Utc::now(),
etag: uuid::Uuid::new_v4().to_string(),
}
}
}
impl From<Contact> for ContactDto {
fn from(contact: Contact) -> Self {
Self {
id: contact.id.to_string(),
address_book_id: contact.address_book_id.to_string(),
uid: contact.uid,
full_name: contact.full_name,
first_name: contact.first_name,
last_name: contact.last_name,
nickname: contact.nickname,
email: contact.email.into_iter().map(EmailDto::from).collect(),
phone: contact.phone.into_iter().map(PhoneDto::from).collect(),
address: contact.address.into_iter().map(AddressDto::from).collect(),
organization: contact.organization,
title: contact.title,
notes: contact.notes,
photo_url: contact.photo_url,
birthday: contact.birthday,
anniversary: contact.anniversary,
created_at: contact.created_at,
updated_at: contact.updated_at,
etag: contact.etag,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateContactDto {
pub address_book_id: String,
pub full_name: Option<String>,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub nickname: Option<String>,
pub email: Vec<EmailDto>,
pub phone: Vec<PhoneDto>,
pub address: Vec<AddressDto>,
pub organization: Option<String>,
pub title: Option<String>,
pub notes: Option<String>,
pub photo_url: Option<String>,
pub birthday: Option<NaiveDate>,
pub anniversary: Option<NaiveDate>,
pub user_id: String, // User creating the contact
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateContactDto {
pub full_name: Option<String>,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub nickname: Option<String>,
pub email: Option<Vec<EmailDto>>,
pub phone: Option<Vec<PhoneDto>>,
pub address: Option<Vec<AddressDto>>,
pub organization: Option<String>,
pub title: Option<String>,
pub notes: Option<String>,
pub photo_url: Option<String>,
pub birthday: Option<NaiveDate>,
pub anniversary: Option<NaiveDate>,
pub user_id: String, // User updating the contact
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateContactVCardDto {
pub address_book_id: String,
pub vcard: String,
pub user_id: String, // User creating the contact
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactGroupDto {
pub id: String,
pub address_book_id: String,
pub name: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub members_count: Option<i32>,
}
impl From<ContactGroup> for ContactGroupDto {
fn from(group: ContactGroup) -> Self {
Self {
id: group.id.to_string(),
address_book_id: group.address_book_id.to_string(),
name: group.name,
created_at: group.created_at,
updated_at: group.updated_at,
members_count: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateContactGroupDto {
pub address_book_id: String,
pub name: String,
pub user_id: String, // User creating the group
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateContactGroupDto {
pub name: String,
pub user_id: String, // User updating the group
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupMembershipDto {
pub group_id: String,
pub contact_id: String,
}
+7 -4
View File
@@ -1,11 +1,14 @@
pub mod address_book_dto;
pub mod calendar_dto;
pub mod contact_dto;
pub mod favorites_dto;
pub mod file_dto;
pub mod folder_dto;
pub mod i18n_dto;
pub mod pagination;
pub mod user_dto;
pub mod trash_dto;
pub mod recent_dto;
pub mod search_dto;
pub mod share_dto;
pub mod favorites_dto;
pub mod recent_dto;
pub mod trash_dto;
pub mod user_dto;
+78
View File
@@ -0,0 +1,78 @@
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use crate::application::dtos::calendar_dto::{
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto,
CreateEventDto, UpdateEventDto, CreateEventICalDto
};
use crate::common::errors::DomainError;
/// 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 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>;
// 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>;
// 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>;
// 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 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 get_events_in_time_range(
&self,
calendar_id: &str,
start: &DateTime<Utc>,
end: &DateTime<Utc>
) -> Result<Vec<CalendarEventDto>, DomainError>;
}
/// Port for calendar use cases
#[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 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>;
// 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>;
// 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 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 get_events_in_range(
&self,
calendar_id: &str,
start: DateTime<Utc>,
end: DateTime<Utc>
) -> Result<Vec<CalendarEventDto>, DomainError>;
}
+57
View File
@@ -0,0 +1,57 @@
use async_trait::async_trait;
use crate::common::errors::DomainError;
use crate::application::dtos::address_book_dto::{
AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto,
ShareAddressBookDto, UnshareAddressBookDto
};
use crate::application::dtos::contact_dto::{
ContactDto, CreateContactDto, UpdateContactDto, CreateContactVCardDto,
ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto
};
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 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_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 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>;
// 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 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>;
// 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>;
// 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>;
}
+8 -6
View File
@@ -1,9 +1,11 @@
pub mod auth_ports;
pub mod calendar_ports;
pub mod carddav_ports;
pub mod favorites_ports;
pub mod file_ports;
pub mod inbound;
pub mod outbound;
pub mod file_ports;
pub mod storage_ports;
pub mod auth_ports;
pub mod trash_ports;
pub mod recent_ports;
pub mod share_ports;
pub mod favorites_ports;
pub mod recent_ports;
pub mod storage_ports;
pub mod trash_ports;
+8
View File
@@ -2,6 +2,7 @@ use std::path::PathBuf;
use async_trait::async_trait;
use bytes::Bytes;
use futures::Stream;
use serde_json::Value;
use crate::domain::entities::file::File;
use crate::domain::services::path_service::StoragePath;
@@ -83,4 +84,11 @@ pub trait StorageUsagePort: Send + Sync + 'static {
/// Actualiza estadísticas de uso de almacenamiento para todos los usuarios
async fn update_all_users_storage_usage(&self) -> Result<(), DomainError>;
}
/// Generic storage service interface for calendar and contact services
#[async_trait]
pub trait StorageUseCase: Send + Sync + 'static {
/// Handle a request with the specified action and parameters
async fn handle_request(&self, action: &str, params: Value) -> Result<Value, DomainError>;
}
@@ -0,0 +1,329 @@
use std::sync::Arc;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::application::dtos::calendar_dto::{
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto,
CreateEventDto, UpdateEventDto, CreateEventICalDto
};
use crate::application::ports::calendar_ports::{CalendarStoragePort, CalendarUseCase};
use crate::interfaces::middleware::auth::CurrentUser;
use crate::common::errors::{DomainError, ErrorKind};
pub struct CalendarService {
calendar_storage: Arc<dyn CalendarStoragePort>,
}
impl CalendarService {
pub fn new(calendar_storage: Arc<dyn CalendarStoragePort>) -> Self {
Self {
calendar_storage,
}
}
}
#[async_trait]
impl CalendarUseCase for CalendarService {
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
}
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
// 3. Update the calendar if they have permission
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?;
if !has_access {
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
}
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?;
if !has_access {
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(&self, calendar_id: &str) -> Result<CalendarDto, DomainError> {
let user_id = "current_user_id"; // This should come from middleware
// Get the calendar
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?;
if !has_access && !calendar.is_public {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"You don't have permission to view this calendar"
));
}
Ok(calendar)
}
async fn list_my_calendars(&self) -> Result<Vec<CalendarDto>, DomainError> {
let user_id = "current_user_id"; // This should come from middleware
self.calendar_storage.list_calendars_by_owner(user_id).await
}
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
}
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
}
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
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
// Only the owner can share the calendar
if calendar.owner_id != current_user_id {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"Only the calendar owner can change sharing settings"
));
}
// Validate access_level
match access_level {
"read" | "write" | "owner" => {},
_ => return Err(DomainError::new(
ErrorKind::InvalidInput,
"Calendar",
format!("Invalid access level: {}. Valid values are: read, write, owner", access_level)
)),
}
self.calendar_storage.share_calendar(calendar_id, user_id, access_level).await
}
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
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
// Only the owner can change sharing settings
if calendar.owner_id != current_user_id {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"Only the calendar owner can change sharing settings"
));
}
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> {
let current_user_id = "current_user_id"; // This should come from middleware
// Check if current user has access
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
// Only the owner can view sharing settings
if calendar.owner_id != current_user_id {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"Only the calendar owner can view sharing settings"
));
}
self.calendar_storage.get_calendar_shares(calendar_id).await
}
async fn create_event(&self, event: CreateEventDto) -> 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?;
if !has_access {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"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> {
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?;
if !has_access {
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 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?;
if !has_access {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
"You don't have permission to update events in this calendar"
));
}
self.calendar_storage.update_event(event_id, update).await
}
async fn delete_event(&self, event_id: &str) -> Result<(), 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?;
if !has_access {
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
}
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError> {
let user_id = "current_user_id"; // This should come from middleware
// Get the event
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?;
// Check if calendar is public
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"
));
}
Ok(event)
}
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?;
// Check if calendar is public
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"
));
}
// Use pagination if provided
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
} else {
self.calendar_storage.list_events_by_calendar(calendar_id).await
}
}
async fn get_events_in_range(
&self,
calendar_id: &str,
start: 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?;
// Check if calendar is public
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"
));
}
self.calendar_storage.get_events_in_time_range(calendar_id, &start, &end).await
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -87,7 +87,7 @@ impl From<FileServiceError> for DomainError {
match err {
FileServiceError::NotFound(id) => DomainError::not_found("File", id),
FileServiceError::Conflict(path) => DomainError::already_exists("File", path),
FileServiceError::InvalidPath(path) => DomainError::validation_error("File", format!("Invalid path: {}", path)),
FileServiceError::InvalidPath(path) => DomainError::validation_error(format!("Invalid path: {}", path)),
FileServiceError::AccessError(msg) => DomainError::access_denied("File", msg),
FileServiceError::InternalError(msg) => DomainError::internal_error("File", msg),
}
+11 -11
View File
@@ -1,21 +1,21 @@
pub mod auth_application_service;
pub mod batch_operations;
pub mod calendar_service;
pub mod contact_service;
pub mod favorites_service;
pub mod file_management_service;
pub mod file_retrieval_service;
pub mod file_service;
pub mod file_upload_service;
pub mod file_use_case_factory;
pub mod folder_service;
pub mod i18n_application_service;
pub mod storage_mediator;
// Nuevos servicios refactorizados
pub mod file_upload_service;
pub mod file_retrieval_service;
pub mod file_management_service;
pub mod file_use_case_factory;
pub mod auth_application_service;
pub mod trash_service;
pub mod recent_service;
pub mod search_service;
pub mod share_service;
pub mod favorites_service;
pub mod recent_service;
pub mod storage_mediator;
pub mod storage_usage_service;
pub mod trash_service;
#[cfg(test)]
mod trash_service_test;
+2 -2
View File
@@ -47,8 +47,8 @@ impl From<ShareServiceError> for DomainError {
ShareServiceError::InvalidPassword(s) => DomainError::access_denied("Share", s),
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("Share", s),
ShareServiceError::Validation(s) => DomainError::validation_error("Share", s),
ShareServiceError::InvalidItemType(s) => DomainError::validation_error(s),
ShareServiceError::Validation(s) => DomainError::validation_error(s),
}
}
}
+9 -9
View File
@@ -89,7 +89,7 @@ impl TrashUseCase for TrashService {
debug!("Getting trash items for user: {}", user_id);
let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
@@ -119,7 +119,7 @@ impl TrashUseCase for TrashService {
},
Err(e) => {
error!("Invalid item UUID: {} - Error: {}", item_id, e);
return Err(DomainError::validation_error("Item", format!("Invalid item ID: {}", e)));
return Err(DomainError::validation_error(format!("Invalid item ID: {}", e)));
}
};
@@ -131,7 +131,7 @@ impl TrashUseCase for TrashService {
},
Err(e) => {
error!("Invalid user UUID: {} - Error: {}", user_id, e);
return Err(DomainError::validation_error("User", format!("Invalid user ID: {}", e)));
return Err(DomainError::validation_error(format!("Invalid user ID: {}", e)));
}
};
@@ -244,7 +244,7 @@ impl TrashUseCase for TrashService {
debug!("Folder moved to trash: {}", item_id);
Ok(())
},
_ => Err(DomainError::validation_error("Item", format!("Invalid item type: {}", item_type))),
_ => Err(DomainError::validation_error(format!("Invalid item type: {}", item_type))),
}
}
@@ -259,7 +259,7 @@ impl TrashUseCase for TrashService {
},
Err(e) => {
error!("Invalid trash ID format: {} - {}", trash_id, e);
return Err(DomainError::validation_error("Trash", format!("Invalid trash ID: {}", e)));
return Err(DomainError::validation_error(format!("Invalid trash ID: {}", e)));
}
};
@@ -270,7 +270,7 @@ impl TrashUseCase for TrashService {
},
Err(e) => {
error!("Invalid user ID format: {} - {}", user_id, e);
return Err(DomainError::validation_error("User", format!("Invalid user ID: {}", e)));
return Err(DomainError::validation_error(format!("Invalid user ID: {}", e)));
}
};
@@ -384,7 +384,7 @@ impl TrashUseCase for TrashService {
},
Err(e) => {
error!("Invalid trash ID format: {} - {}", trash_id, e);
return Err(DomainError::validation_error("Trash", format!("Invalid trash ID: {}", e)));
return Err(DomainError::validation_error(format!("Invalid trash ID: {}", e)));
}
};
@@ -395,7 +395,7 @@ impl TrashUseCase for TrashService {
},
Err(e) => {
error!("Invalid user ID format: {} - {}", user_id, e);
return Err(DomainError::validation_error("User", format!("Invalid user ID: {}", e)));
return Err(DomainError::validation_error(format!("Invalid user ID: {}", e)));
}
};
@@ -500,7 +500,7 @@ impl TrashUseCase for TrashService {
info!("Emptying trash for user {}", user_id);
let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
// Get all items in the user's trash
let items = self.trash_repository.get_trash_items(&user_uuid).await?;