implement propfind sync-token
This commit is contained in:
parent
171a762768
commit
18f2154151
7 changed files with 195 additions and 40 deletions
|
@ -56,6 +56,11 @@ impl Calendar {
|
||||||
self.internal.read().await.davdag.state().clone()
|
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
|
/// The diff API is a write API as we might need to push a merge node
|
||||||
/// to get a new sync token
|
/// to get a new sync token
|
||||||
pub async fn diff(&self, sync_token: Token) -> Result<(Token, Vec<SyncChange>)> {
|
pub async fn diff(&self, sync_token: Token) -> Result<(Token, Vec<SyncChange>)> {
|
||||||
|
@ -174,6 +179,12 @@ impl CalendarInternal {
|
||||||
.map(|s| s.clone())
|
.map(|s| s.clone())
|
||||||
.collect();
|
.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 heads = davstate.heads_vec();
|
||||||
let token = match heads.as_slice() {
|
let token = match heads.as_slice() {
|
||||||
[token] => *token,
|
[token] => *token,
|
||||||
|
@ -184,7 +195,6 @@ impl CalendarInternal {
|
||||||
token
|
token
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Ok(token)
|
||||||
Ok((token, changes))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,11 @@ use super::xml::{IRead, QRead, Reader, DAV_URN};
|
||||||
|
|
||||||
impl QRead<PropertyRequest> for PropertyRequest {
|
impl QRead<PropertyRequest> for PropertyRequest {
|
||||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||||
let mut dirty = false;
|
if xml.maybe_open(DAV_URN, "sync-token").await?.is_some() {
|
||||||
let mut m_cdr = None;
|
xml.close().await?;
|
||||||
xml.maybe_read(&mut m_cdr, &mut dirty).await?;
|
return Ok(Self::SyncToken);
|
||||||
m_cdr.ok_or(ParsingError::Recoverable).map(Self::SyncToken)
|
}
|
||||||
|
return Err(ParsingError::Recoverable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +89,6 @@ impl QRead<SyncTokenRequest> for SyncTokenRequest {
|
||||||
|
|
||||||
impl QRead<SyncToken> for SyncToken {
|
impl QRead<SyncToken> for SyncToken {
|
||||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||||
println!("sync_token {:?}", xml.peek());
|
|
||||||
xml.open(DAV_URN, "sync-token").await?;
|
xml.open(DAV_URN, "sync-token").await?;
|
||||||
let token = xml.tag_string().await?;
|
let token = xml.tag_string().await?;
|
||||||
xml.close().await?;
|
xml.close().await?;
|
||||||
|
@ -213,9 +213,7 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn prop_req() {
|
async fn prop_req() {
|
||||||
let expected = dav::PropName::<All>(vec![dav::PropertyRequest::Extension(
|
let expected = dav::PropName::<All>(vec![dav::PropertyRequest::Extension(
|
||||||
realization::PropertyRequest::Sync(PropertyRequest::SyncToken(
|
realization::PropertyRequest::Sync(PropertyRequest::SyncToken),
|
||||||
SyncTokenRequest::InitialSync,
|
|
||||||
)),
|
|
||||||
)]);
|
)]);
|
||||||
let src = r#"<prop xmlns="DAV:"><sync-token/></prop>"#;
|
let src = r#"<prop xmlns="DAV:"><sync-token/></prop>"#;
|
||||||
let got = deserialize::<dav::PropName<All>>(src).await;
|
let got = deserialize::<dav::PropName<All>>(src).await;
|
||||||
|
|
|
@ -16,7 +16,10 @@ impl QWrite for Property {
|
||||||
impl QWrite for PropertyRequest {
|
impl QWrite for PropertyRequest {
|
||||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||||
match self {
|
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() {
|
async fn prop_req() {
|
||||||
serialize_deserialize(&dav::PropName::<All>(vec![
|
serialize_deserialize(&dav::PropName::<All>(vec![
|
||||||
dav::PropertyRequest::Extension(realization::PropertyRequest::Sync(
|
dav::PropertyRequest::Extension(realization::PropertyRequest::Sync(
|
||||||
PropertyRequest::SyncToken(SyncTokenRequest::InitialSync),
|
PropertyRequest::SyncToken,
|
||||||
)),
|
)),
|
||||||
]))
|
]))
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -6,7 +6,7 @@ use super::versioningtypes as vers;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub enum PropertyRequest {
|
pub enum PropertyRequest {
|
||||||
SyncToken(SyncTokenRequest),
|
SyncToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
|
|
@ -21,7 +21,7 @@ impl QRead<PropertyRequest> for PropertyRequest {
|
||||||
impl<E: dav::Extension> QRead<Property<E>> for Property<E> {
|
impl<E: dav::Extension> QRead<Property<E>> for Property<E> {
|
||||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||||
if xml
|
if xml
|
||||||
.maybe_open(DAV_URN, "supported-report-set")
|
.maybe_open_start(DAV_URN, "supported-report-set")
|
||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,7 +14,9 @@ use aero_collections::{
|
||||||
use aero_dav::acltypes as acl;
|
use aero_dav::acltypes as acl;
|
||||||
use aero_dav::caltypes as cal;
|
use aero_dav::caltypes as cal;
|
||||||
use aero_dav::realization::{self as all, All};
|
use aero_dav::realization::{self as all, All};
|
||||||
|
use aero_dav::synctypes as sync;
|
||||||
use aero_dav::types as dav;
|
use aero_dav::types as dav;
|
||||||
|
use aero_dav::versioningtypes as vers;
|
||||||
|
|
||||||
use super::node::PropertyStream;
|
use super::node::PropertyStream;
|
||||||
use crate::dav::node::{Content, DavNode, PutPolicy};
|
use crate::dav::node::{Content, DavNode, PutPolicy};
|
||||||
|
@ -431,38 +433,78 @@ impl DavNode for CalendarNode {
|
||||||
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
|
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
|
||||||
cal::PropertyRequest::SupportedCalendarComponentSet,
|
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> {
|
fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
|
||||||
let calname = self.calname.to_string();
|
let calname = self.calname.to_string();
|
||||||
|
let col = self.col.clone();
|
||||||
|
|
||||||
futures::stream::iter(prop.0)
|
futures::stream::iter(prop.0)
|
||||||
.map(move |n| {
|
.then(move |n| {
|
||||||
let prop = match n {
|
let calname = calname.clone();
|
||||||
dav::PropertyRequest::DisplayName => {
|
let col = col.clone();
|
||||||
dav::Property::DisplayName(format!("{} calendar", calname))
|
|
||||||
}
|
async move {
|
||||||
dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![
|
let prop = match n {
|
||||||
dav::ResourceType::Collection,
|
dav::PropertyRequest::DisplayName => {
|
||||||
dav::ResourceType::Extension(all::ResourceType::Cal(
|
dav::Property::DisplayName(format!("{} calendar", calname))
|
||||||
cal::ResourceType::Calendar,
|
}
|
||||||
|
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::Extension(all::PropertyRequest::Sync(
|
||||||
//dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())),
|
sync::PropertyRequest::SyncToken,
|
||||||
//@FIXME seems wrong but seems to be what Thunderbird expects...
|
)) => match col.token().await {
|
||||||
dav::PropertyRequest::GetContentType => {
|
Ok(token) => dav::Property::Extension(all::Property::Sync(
|
||||||
dav::Property::GetContentType("text/calendar".into())
|
sync::Property::SyncToken(sync::SyncToken(format!(
|
||||||
}
|
"https://aerogramme.0/sync/{}",
|
||||||
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
|
token
|
||||||
cal::PropertyRequest::SupportedCalendarComponentSet,
|
))),
|
||||||
)) => dav::Property::Extension(all::Property::Cal(
|
)),
|
||||||
cal::Property::SupportedCalendarComponentSet(vec![cal::CompSupport(
|
_ => return Err(n.clone()),
|
||||||
cal::Component::VEvent,
|
},
|
||||||
)]),
|
dav::PropertyRequest::Extension(all::PropertyRequest::Vers(
|
||||||
)),
|
vers::PropertyRequest::SupportedReportSet,
|
||||||
v => return Err(v),
|
)) => dav::Property::Extension(all::Property::Vers(
|
||||||
};
|
vers::Property::SupportedReportSet(vec![
|
||||||
Ok(prop)
|
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()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ fn main() {
|
||||||
rfc4918_webdav_core();
|
rfc4918_webdav_core();
|
||||||
rfc5397_webdav_principal();
|
rfc5397_webdav_principal();
|
||||||
rfc4791_webdav_caldav();
|
rfc4791_webdav_caldav();
|
||||||
|
rfc6578_webdav_sync();
|
||||||
println!("✅ SUCCESS 🌟🚀🥳🙏🥹");
|
println!("✅ SUCCESS 🌟🚀🥳🙏🥹");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,7 +366,9 @@ fn rfc5819_imapext_liststatus() {
|
||||||
use aero_dav::acltypes as acl;
|
use aero_dav::acltypes as acl;
|
||||||
use aero_dav::caltypes as cal;
|
use aero_dav::caltypes as cal;
|
||||||
use aero_dav::realization::{self, All};
|
use aero_dav::realization::{self, All};
|
||||||
|
use aero_dav::synctypes as sync;
|
||||||
use aero_dav::types as dav;
|
use aero_dav::types as dav;
|
||||||
|
use aero_dav::versioningtypes as vers;
|
||||||
|
|
||||||
use crate::common::dav_deserialize;
|
use crate::common::dav_deserialize;
|
||||||
|
|
||||||
|
@ -1011,4 +1014,103 @@ fn rfc4791_webdav_caldav() {
|
||||||
.expect("test fully run")
|
.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")
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue