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()
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -6,7 +6,7 @@ use super::versioningtypes as vers;
|
|||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum PropertyRequest {
|
||||
SyncToken(SyncTokenRequest),
|
||||
SyncToken,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue