implement propfind sync-token

This commit is contained in:
Quentin 2024-05-28 16:03:25 +02:00
parent 171a762768
commit 18f2154151
Signed by: quentin
GPG key ID: E9602264D639FF68
7 changed files with 195 additions and 40 deletions

View file

@ -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)
}
}

View file

@ -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;

View file

@ -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;

View file

@ -6,7 +6,7 @@ use super::versioningtypes as vers;
#[derive(Debug, PartialEq, Clone)]
pub enum PropertyRequest {
SyncToken(SyncTokenRequest),
SyncToken,
}
#[derive(Debug, PartialEq, Clone)]

View file

@ -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()
{

View file

@ -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()
}

View file

@ -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")
}