aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-05-28 16:03:25 +0200
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-05-28 16:03:25 +0200
commit18f2154151b2cf81e03bdda28fa2ea5d685e33d1 (patch)
treea6fae34924e2808eb65276919d671718adbfbd44
parent171a762768aabd799a1012d2fb939d869ff53f7b (diff)
downloadaerogramme-18f2154151b2cf81e03bdda28fa2ea5d685e33d1.tar.gz
aerogramme-18f2154151b2cf81e03bdda28fa2ea5d685e33d1.zip
implement propfind sync-token
-rw-r--r--aero-collections/src/calendar/mod.rs14
-rw-r--r--aero-dav/src/syncdecoder.rs14
-rw-r--r--aero-dav/src/syncencoder.rs7
-rw-r--r--aero-dav/src/synctypes.rs2
-rw-r--r--aero-dav/src/versioningdecoder.rs2
-rw-r--r--aero-proto/src/dav/resource.rs92
-rw-r--r--aerogramme/tests/behavior.rs104
7 files changed, 195 insertions, 40 deletions
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<Token> {
+ 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<SyncChange>)> {
@@ -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<Token> {
+ 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<PropertyRequest> for PropertyRequest {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
- 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<SyncTokenRequest> for SyncTokenRequest {
impl QRead<SyncToken> for SyncToken {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
- 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::<All>(vec![dav::PropertyRequest::Extension(
- realization::PropertyRequest::Sync(PropertyRequest::SyncToken(
- SyncTokenRequest::InitialSync,
- )),
+ realization::PropertyRequest::Sync(PropertyRequest::SyncToken),
)]);
let src = r#"<prop xmlns="DAV:"><sync-token/></prop>"#;
let got = deserialize::<dav::PropName<All>>(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<impl IWrite>) -> 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::<All>(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<PropertyRequest> for PropertyRequest {
impl<E: dav::Extension> QRead<Property<E>> for Property<E> {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
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<All>) -> 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 <sync-token/> + <supported-report-set/> (2nd element is theoretically from versioning)
+ let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><propname/></propfind>"#;
+ 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::<dav::Multistatus<All>>(&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#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><prop><sync-token/><supported-report-set/></prop></propfind>"#;
+ 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::<dav::Multistatus<All>>(&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::<dav::Multistatus<All>>(&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::<dav::Multistatus<All>>(&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")
+}