diff --git a/aero-collections/src/calendar/mod.rs b/aero-collections/src/calendar/mod.rs index cd05328..414426a 100644 --- a/aero-collections/src/calendar/mod.rs +++ b/aero-collections/src/calendar/mod.rs @@ -56,6 +56,11 @@ impl Calendar { self.internal.read().await.davdag.state().clone() } + /// Access the current token + pub async fn token(&self) -> Result { + self.internal.write().await.current_token().await + } + /// The diff API is a write API as we might need to push a merge node /// to get a new sync token pub async fn diff(&self, sync_token: Token) -> Result<(Token, Vec)> { @@ -174,6 +179,12 @@ impl CalendarInternal { .map(|s| s.clone()) .collect(); + let token = self.current_token().await?; + Ok((token, changes)) + } + + async fn current_token(&mut self) -> Result { + let davstate = self.davdag.state(); let heads = davstate.heads_vec(); let token = match heads.as_slice() { [token] => *token, @@ -184,7 +195,6 @@ impl CalendarInternal { token } }; - - Ok((token, changes)) + Ok(token) } } diff --git a/aero-dav/src/syncdecoder.rs b/aero-dav/src/syncdecoder.rs index be25b79..2a61dea 100644 --- a/aero-dav/src/syncdecoder.rs +++ b/aero-dav/src/syncdecoder.rs @@ -7,10 +7,11 @@ use super::xml::{IRead, QRead, Reader, DAV_URN}; impl QRead for PropertyRequest { async fn qread(xml: &mut Reader) -> Result { - let mut dirty = false; - let mut m_cdr = None; - xml.maybe_read(&mut m_cdr, &mut dirty).await?; - m_cdr.ok_or(ParsingError::Recoverable).map(Self::SyncToken) + if xml.maybe_open(DAV_URN, "sync-token").await?.is_some() { + xml.close().await?; + return Ok(Self::SyncToken); + } + return Err(ParsingError::Recoverable); } } @@ -88,7 +89,6 @@ impl QRead for SyncTokenRequest { impl QRead for SyncToken { async fn qread(xml: &mut Reader) -> Result { - println!("sync_token {:?}", xml.peek()); xml.open(DAV_URN, "sync-token").await?; let token = xml.tag_string().await?; xml.close().await?; @@ -213,9 +213,7 @@ mod tests { #[tokio::test] async fn prop_req() { let expected = dav::PropName::(vec![dav::PropertyRequest::Extension( - realization::PropertyRequest::Sync(PropertyRequest::SyncToken( - SyncTokenRequest::InitialSync, - )), + realization::PropertyRequest::Sync(PropertyRequest::SyncToken), )]); let src = r#""#; let got = deserialize::>(src).await; diff --git a/aero-dav/src/syncencoder.rs b/aero-dav/src/syncencoder.rs index 8badc92..2dd50eb 100644 --- a/aero-dav/src/syncencoder.rs +++ b/aero-dav/src/syncencoder.rs @@ -16,7 +16,10 @@ impl QWrite for Property { impl QWrite for PropertyRequest { async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { match self { - Self::SyncToken(token) => token.qwrite(xml).await, + Self::SyncToken => { + let start = xml.create_dav_element("sync-token"); + xml.q.write_event_async(Event::Empty(start)).await + } } } } @@ -180,7 +183,7 @@ mod tests { async fn prop_req() { serialize_deserialize(&dav::PropName::(vec![ dav::PropertyRequest::Extension(realization::PropertyRequest::Sync( - PropertyRequest::SyncToken(SyncTokenRequest::InitialSync), + PropertyRequest::SyncToken, )), ])) .await; diff --git a/aero-dav/src/synctypes.rs b/aero-dav/src/synctypes.rs index cbd86b8..2a14221 100644 --- a/aero-dav/src/synctypes.rs +++ b/aero-dav/src/synctypes.rs @@ -6,7 +6,7 @@ use super::versioningtypes as vers; #[derive(Debug, PartialEq, Clone)] pub enum PropertyRequest { - SyncToken(SyncTokenRequest), + SyncToken, } #[derive(Debug, PartialEq, Clone)] diff --git a/aero-dav/src/versioningdecoder.rs b/aero-dav/src/versioningdecoder.rs index c28c0d5..a0a3ddf 100644 --- a/aero-dav/src/versioningdecoder.rs +++ b/aero-dav/src/versioningdecoder.rs @@ -21,7 +21,7 @@ impl QRead for PropertyRequest { impl QRead> for Property { async fn qread(xml: &mut Reader) -> Result { if xml - .maybe_open(DAV_URN, "supported-report-set") + .maybe_open_start(DAV_URN, "supported-report-set") .await? .is_some() { diff --git a/aero-proto/src/dav/resource.rs b/aero-proto/src/dav/resource.rs index 04bae4f..1ae766c 100644 --- a/aero-proto/src/dav/resource.rs +++ b/aero-proto/src/dav/resource.rs @@ -14,7 +14,9 @@ use aero_collections::{ use aero_dav::acltypes as acl; use aero_dav::caltypes as cal; use aero_dav::realization::{self as all, All}; +use aero_dav::synctypes as sync; use aero_dav::types as dav; +use aero_dav::versioningtypes as vers; use super::node::PropertyStream; use crate::dav::node::{Content, DavNode, PutPolicy}; @@ -431,38 +433,78 @@ impl DavNode for CalendarNode { dav::PropertyRequest::Extension(all::PropertyRequest::Cal( cal::PropertyRequest::SupportedCalendarComponentSet, )), + dav::PropertyRequest::Extension(all::PropertyRequest::Sync( + sync::PropertyRequest::SyncToken, + )), + dav::PropertyRequest::Extension(all::PropertyRequest::Vers( + vers::PropertyRequest::SupportedReportSet, + )), ]) } fn properties(&self, _user: &ArcUser, prop: dav::PropName) -> PropertyStream<'static> { let calname = self.calname.to_string(); + let col = self.col.clone(); futures::stream::iter(prop.0) - .map(move |n| { - let prop = match n { - dav::PropertyRequest::DisplayName => { - dav::Property::DisplayName(format!("{} calendar", calname)) - } - dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![ - dav::ResourceType::Collection, - dav::ResourceType::Extension(all::ResourceType::Cal( - cal::ResourceType::Calendar, + .then(move |n| { + let calname = calname.clone(); + let col = col.clone(); + + async move { + let prop = match n { + dav::PropertyRequest::DisplayName => { + dav::Property::DisplayName(format!("{} calendar", calname)) + } + dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![ + dav::ResourceType::Collection, + dav::ResourceType::Extension(all::ResourceType::Cal( + cal::ResourceType::Calendar, + )), + ]), + //dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())), + //@FIXME seems wrong but seems to be what Thunderbird expects... + dav::PropertyRequest::GetContentType => { + dav::Property::GetContentType("text/calendar".into()) + } + dav::PropertyRequest::Extension(all::PropertyRequest::Cal( + cal::PropertyRequest::SupportedCalendarComponentSet, + )) => dav::Property::Extension(all::Property::Cal( + cal::Property::SupportedCalendarComponentSet(vec![ + cal::CompSupport(cal::Component::VEvent), + cal::CompSupport(cal::Component::VTodo), + cal::CompSupport(cal::Component::VJournal), + ]), )), - ]), - //dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())), - //@FIXME seems wrong but seems to be what Thunderbird expects... - dav::PropertyRequest::GetContentType => { - dav::Property::GetContentType("text/calendar".into()) - } - dav::PropertyRequest::Extension(all::PropertyRequest::Cal( - cal::PropertyRequest::SupportedCalendarComponentSet, - )) => dav::Property::Extension(all::Property::Cal( - cal::Property::SupportedCalendarComponentSet(vec![cal::CompSupport( - cal::Component::VEvent, - )]), - )), - v => return Err(v), - }; - Ok(prop) + dav::PropertyRequest::Extension(all::PropertyRequest::Sync( + sync::PropertyRequest::SyncToken, + )) => match col.token().await { + Ok(token) => dav::Property::Extension(all::Property::Sync( + sync::Property::SyncToken(sync::SyncToken(format!( + "https://aerogramme.0/sync/{}", + token + ))), + )), + _ => return Err(n.clone()), + }, + dav::PropertyRequest::Extension(all::PropertyRequest::Vers( + vers::PropertyRequest::SupportedReportSet, + )) => dav::Property::Extension(all::Property::Vers( + vers::Property::SupportedReportSet(vec![ + vers::SupportedReport(vers::ReportName::Extension( + all::ReportTypeName::Cal(cal::ReportTypeName::Multiget), + )), + vers::SupportedReport(vers::ReportName::Extension( + all::ReportTypeName::Cal(cal::ReportTypeName::Query), + )), + vers::SupportedReport(vers::ReportName::Extension( + all::ReportTypeName::Sync(sync::ReportTypeName::SyncCollection), + )), + ]), + )), + v => return Err(v), + }; + Ok(prop) + } }) .boxed() } diff --git a/aerogramme/tests/behavior.rs b/aerogramme/tests/behavior.rs index 1097fe7..1846c92 100644 --- a/aerogramme/tests/behavior.rs +++ b/aerogramme/tests/behavior.rs @@ -20,6 +20,7 @@ fn main() { rfc4918_webdav_core(); rfc5397_webdav_principal(); rfc4791_webdav_caldav(); + rfc6578_webdav_sync(); println!("โœ… SUCCESS ๐ŸŒŸ๐Ÿš€๐Ÿฅณ๐Ÿ™๐Ÿฅน"); } @@ -365,7 +366,9 @@ fn rfc5819_imapext_liststatus() { use aero_dav::acltypes as acl; use aero_dav::caltypes as cal; use aero_dav::realization::{self, All}; +use aero_dav::synctypes as sync; use aero_dav::types as dav; +use aero_dav::versioningtypes as vers; use crate::common::dav_deserialize; @@ -1011,4 +1014,103 @@ fn rfc4791_webdav_caldav() { .expect("test fully run") } -// @TODO SYNC +fn rfc6578_webdav_sync() { + println!("๐Ÿงช rfc6578_webdav_sync"); + common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| { + // propname on a calendar node must return + (2nd element is theoretically from versioning) + let propfind_req = r#""#; + let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").body(propfind_req).send()?.text()?; + let multistatus = dav_deserialize::>(&body); + let root_propstats = multistatus.responses.iter() + .find_map(|v| match &v.status_or_propstat { + dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/calendar/Personal/" => Some(x), + _ => None, + }) + .expect("propstats for target must exist"); + let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("some propstats for root must be 200"); + assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Request(dav::PropertyRequest::Extension( + realization::PropertyRequest::Sync(sync::PropertyRequest::SyncToken) + )))).is_some()); + assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Request(dav::PropertyRequest::Extension( + realization::PropertyRequest::Vers(vers::PropertyRequest::SupportedReportSet) + )))).is_some()); + + // synctoken and supported report set must contains a meaningful value when queried + let propfind_req = r#""#; + let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").body(propfind_req).send()?.text()?; + let multistatus = dav_deserialize::>(&body); + let root_propstats = multistatus.responses.iter() + .find_map(|v| match &v.status_or_propstat { + dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/calendar/Personal/" => Some(x), + _ => None, + }) + .expect("propstats for target must exist"); + + let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("some propstats for root must be 200"); + + let init_sync_token = root_success.prop.0.iter().find_map(|p| match p { + dav::AnyProperty::Value(dav::Property::Extension(realization::Property::Sync(sync::Property::SyncToken(st)))) => Some(st), + _ => None, + }).expect("sync_token exists"); + + let supported = root_success.prop.0.iter().find_map(|p| match p { + dav::AnyProperty::Value(dav::Property::Extension(realization::Property::Vers(vers::Property::SupportedReportSet(s)))) => Some(s), + _ => None + }).expect("supported report set exists"); + assert_eq!(&supported[..], &[ + vers::SupportedReport(vers::ReportName::Extension(realization::ReportTypeName::Cal(cal::ReportTypeName::Multiget))), + vers::SupportedReport(vers::ReportName::Extension(realization::ReportTypeName::Cal(cal::ReportTypeName::Query))), + vers::SupportedReport(vers::ReportName::Extension(realization::ReportTypeName::Sync(sync::ReportTypeName::SyncCollection))), + ]); + + + // synctoken must change if we add a file + let resp = http + .put("http://localhost:8087/alice/calendar/Personal/rfc1.ics") + .header("If-None-Match", "*") + .body(ICAL_RFC1) + .send()?; + assert_eq!(resp.status(), 201); + + let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").body(propfind_req).send()?.text()?; + let multistatus = dav_deserialize::>(&body); + + let root_propstats = multistatus.responses.iter() + .find_map(|v| match &v.status_or_propstat { + dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/calendar/Personal/" => Some(x), + _ => None, + }) + .expect("propstats for target must exist"); + let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("some propstats for root must be 200"); + let rfc1_sync_token = root_success.prop.0.iter().find_map(|p| match p { + dav::AnyProperty::Value(dav::Property::Extension(realization::Property::Sync(sync::Property::SyncToken(st)))) => Some(st), + _ => None, + }).expect("sync_token exists"); + assert!(init_sync_token != rfc1_sync_token); + + + // synctoken must change if we delete a file + let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc1.ics").send()?; + assert_eq!(resp.status(), 204); + + let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").body(propfind_req).send()?.text()?; + let multistatus = dav_deserialize::>(&body); + + let root_propstats = multistatus.responses.iter() + .find_map(|v| match &v.status_or_propstat { + dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/calendar/Personal/" => Some(x), + _ => None, + }) + .expect("propstats for target must exist"); + let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("some propstats for root must be 200"); + let del_sync_token = root_success.prop.0.iter().find_map(|p| match p { + dav::AnyProperty::Value(dav::Property::Extension(realization::Property::Sync(sync::Property::SyncToken(st)))) => Some(st), + _ => None, + }).expect("sync_token exists"); + assert!(init_sync_token != del_sync_token); + assert!(rfc1_sync_token != del_sync_token); + + Ok(()) + }) + .expect("test fully run") +}