diff --git a/crates/dav/src/calendar/freebusy.rs b/crates/dav/src/calendar/freebusy.rs index dd6c677e..dec28eea 100644 --- a/crates/dav/src/calendar/freebusy.rs +++ b/crates/dav/src/calendar/freebusy.rs @@ -124,6 +124,9 @@ impl CalendarFreebusyRequestHandler for Server { .map(|resource| resource.document_id) .collect::>(); + let mut fb_entries: AHashMap> = + AHashMap::with_capacity(document_ids.len()); + for document_id in document_ids { let archive = if let Some(archive) = self .get_archive(account_id, Collection::CalendarEvent, document_id) @@ -192,45 +195,38 @@ impl CalendarFreebusyRequestHandler for Server { if event.comp_id == component_id && range.is_in_range(false, event.start, event.end) { - events_in_range.push(ICalendarValue::Period( - ICalendarPeriod::Range { - start: PartialDateTime::from_utc_timestamp(event.start), - end: PartialDateTime::from_utc_timestamp(event.end), - }, - )); + events_in_range.push((event.start, event.end)); } } if !events_in_range.is_empty() { - entries.push(ICalendarEntry { - name: ICalendarProperty::Freebusy, - params: vec![ICalendarParameter::Fbtype(fbtype)], - values: events_in_range, - }); + fb_entries + .entry(fbtype) + .or_default() + .extend(events_in_range); } } ArchivedICalendarComponentType::VFreebusy => { for entry in component.entries.iter() { if matches!(entry.name, ArchivedICalendarProperty::Freebusy) { let mut fb_in_range = - freebusy_in_range(entry, &range, true, default_tz) - .peekable(); + freebusy_in_range_utc(entry, &range, default_tz).peekable(); if fb_in_range.peek().is_some() { - entries.push(ICalendarEntry { - name: ICalendarProperty::Freebusy, - params: entry - .params - .iter() - .filter(|param| { - matches!( - param, - ArchivedICalendarParameter::Fbtype(_) - ) - }) - .filter_map(|v| rkyv_deserialize(v).ok()) - .collect(), - values: fb_in_range.collect(), - }); + let fb_type = entry + .params + .iter() + .find_map(|param| { + if let ArchivedICalendarParameter::Fbtype(param) = + param + { + rkyv_deserialize(param).ok() + } else { + None + } + }) + .unwrap_or(ICalendarFreeBusyType::Busy); + + fb_entries.entry(fb_type).or_default().extend(fb_in_range); } } } @@ -239,6 +235,14 @@ impl CalendarFreebusyRequestHandler for Server { } } } + + for (fbtype, events_in_range) in fb_entries { + entries.push(ICalendarEntry { + name: ICalendarProperty::Freebusy, + params: vec![ICalendarParameter::Fbtype(fbtype)], + values: merge_intervals(events_in_range), + }); + } } // Build ICalendar @@ -275,10 +279,44 @@ impl CalendarFreebusyRequestHandler for Server { } } +fn merge_intervals(mut intervals: Vec<(i64, i64)>) -> Vec { + if intervals.len() > 1 { + intervals.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut unique_intervals = Vec::new(); + let mut start_time = intervals[0].0; + let mut end_time = intervals[0].1; + + for &(curr_start, curr_end) in intervals.iter().skip(1) { + if curr_start <= end_time { + end_time = end_time.max(curr_end); + } else { + unique_intervals.push(build_ical_value(start_time, end_time)); + start_time = curr_start; + end_time = curr_end; + } + } + + unique_intervals.push(build_ical_value(start_time, end_time)); + unique_intervals + } else { + intervals + .into_iter() + .map(|(start, end)| build_ical_value(start, end)) + .collect() + } +} + +fn build_ical_value(from: i64, to: i64) -> ICalendarValue { + ICalendarValue::Period(ICalendarPeriod::Range { + start: PartialDateTime::from_utc_timestamp(from), + end: PartialDateTime::from_utc_timestamp(to), + }) +} + pub(crate) fn freebusy_in_range( entry: &ArchivedICalendarEntry, range: &TimeRange, - to_utc: bool, default_tz: Tz, ) -> impl Iterator { let tz = entry @@ -292,15 +330,34 @@ pub(crate) fn freebusy_in_range( let start = start.timestamp(); let end = end.timestamp(); if range.is_in_range(false, start, end) { - if to_utc { - ICalendarValue::Period(ICalendarPeriod::Range { - start: PartialDateTime::from_utc_timestamp(start), - end: PartialDateTime::from_utc_timestamp(end), - }) - .into() - } else { - rkyv_deserialize(value).ok() - } + rkyv_deserialize(value).ok() + } else { + None + } + }) + } else { + None + } + }) +} + +fn freebusy_in_range_utc( + entry: &ArchivedICalendarEntry, + range: &TimeRange, + default_tz: Tz, +) -> impl Iterator { + let tz = entry + .tz_id() + .and_then(|tz_id| Tz::from_str(tz_id).ok()) + .unwrap_or(default_tz); + + entry.values.iter().filter_map(move |value| { + if let ArchivedICalendarValue::Period(period) = &value { + period.time_range(tz).and_then(|(start, end)| { + let start = start.timestamp(); + let end = end.timestamp(); + if range.is_in_range(false, start, end) { + Some((start, end)) } else { None } diff --git a/crates/dav/src/calendar/query.rs b/crates/dav/src/calendar/query.rs index 03c4e63c..db3fd32d 100644 --- a/crates/dav/src/calendar/query.rs +++ b/crates/dav/src/calendar/query.rs @@ -574,8 +574,7 @@ impl CalendarQueryHandler { for (entry, with_value) in entries { if matches!(entry.name, ArchivedICalendarProperty::Freebusy) { let mut fb_in_range = - freebusy_in_range(entry, &range, false, self.default_tz) - .peekable(); + freebusy_in_range(entry, &range, self.default_tz).peekable(); if fb_in_range.peek().is_none() { continue; } else { diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs index 154ac8f6..8b2b2cd9 100644 --- a/crates/dav/src/common/lock.rs +++ b/crates/dav/src/common/lock.rs @@ -562,30 +562,35 @@ impl LockRequestHandler for Server { .await .caused_by(trc::location!())? .modseq; - resource_state.sync_token = - Some(Urn::Sync(change_id.unwrap_or_default()).to_string()); + resource_state.sync_token = Some( + Urn::Sync { + id: change_id.unwrap_or_default(), + seq: 0, + } + .to_string(), + ); } } for cond in &if_.list { match cond { - Condition::StateToken { is_not, token } - if token.starts_with("urn:stalwart:davsync:") => - { - if !((resource_state - .sync_token - .as_ref() - .is_some_and(|sync_token| sync_token == token)) + Condition::StateToken { is_not, token } => { + if let Some(token) = Urn::try_extract_sync_id(token) { + if !((resource_state + .sync_token + .as_deref() + .and_then(Urn::try_extract_sync_id) + .is_some_and(|sync_token| sync_token == token)) + ^ is_not) + { + continue 'outer; + } + } else if !((resource_state.lock_tokens.iter().any(|t| t == token)) ^ is_not) { continue 'outer; } } - Condition::StateToken { is_not, token } => { - if !((resource_state.lock_tokens.iter().any(|t| t == token)) ^ is_not) { - continue 'outer; - } - } Condition::ETag { is_not, tag } => { if !((resource_state.etag.as_ref().is_some_and(|etag| etag == tag)) ^ is_not) diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index 8d8fa358..0fa5a335 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -56,7 +56,10 @@ pub(crate) enum SyncType { #[default] None, Initial, - From(u64), + From { + id: u64, + seq: u32, + }, } #[derive(Default, Debug)] @@ -233,7 +236,7 @@ impl<'x> DavQuery<'x> { .as_deref() .and_then(Urn::parse) .and_then(|urn| urn.try_unwrap_sync()) - .map(SyncType::From) + .map(|(id, seq)| SyncType::From { id, seq }) .unwrap_or(SyncType::Initial), depth: match changes.depth { Depth::One => 1, diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index 742fcdba..e8a17492 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -353,6 +353,11 @@ impl PropFindRequestHandler for Server { let mut ctag = None; let mut paths; let mut query_filter = None; + let mut limit = std::cmp::min( + query.limit.unwrap_or(u32::MAX) as usize, + self.core.groupware.max_results, + ); + let mut is_sync_limited = false; //let c = println!("handling DAV query {query:#?}"); @@ -405,42 +410,29 @@ impl PropFindRequestHandler for Server { // Filter by changelog match query.sync_type { - SyncType::From(change_id) => { + SyncType::From { id, seq } => { let container_changes = self .store() - .changes(account_id, collection_container, Query::Since(change_id)) + .changes(account_id, collection_container, Query::Since(id)) .await .caused_by(trc::location!())?; let children_changes = if container_has_children { self.store() - .changes(account_id, collection_children, Query::Since(change_id)) + .changes(account_id, collection_children, Query::Since(id)) .await .caused_by(trc::location!())? .into() } else { None }; - let change_id = std::cmp::max( - container_changes.to_change_id, - children_changes.as_ref().map_or(0, |c| c.to_change_id), - ); - - // Set sync token - let sync_token = if change_id != 0 { - let sync_token = Urn::Sync(change_id).to_string(); - data.accounts.entry(account_id).or_default().sync_token = - sync_token.clone().into(); - sync_token - } else { - data.sync_token(self, account_id, collection_container) - .await - .caused_by(trc::location!())? - }; - response.set_sync_token(sync_token); + // Merge changes + let mut total_changes = 0; for (changes, document_ids) in [ - Some((container_changes, &mut display_containers)), - children_changes.map(|changes| (changes, &mut display_children)), + Some((&container_changes, &mut display_containers)), + children_changes + .as_ref() + .map(|changes| (changes, &mut display_children)), ] .into_iter() .flatten() @@ -453,10 +445,63 @@ impl PropFindRequestHandler for Server { ); if let Some(document_ids) = document_ids { *document_ids &= changes; + total_changes += document_ids.len() as usize; } else { + total_changes += changes.len() as usize; *document_ids = Some(changes); } } + + // Truncate changes + if total_changes > limit { + let mut offset = limit * seq as usize; + let mut total_changes = 0; + for document_ids in [&mut display_containers, &mut display_children] + .into_iter() + .flatten() + { + let mut new_document_ids = RoaringBitmap::new(); + for id in document_ids.iter() { + if offset > 0 { + offset -= 1; + } else if total_changes < limit { + new_document_ids.insert(id); + total_changes += 1; + } else { + is_sync_limited = true; + } + } + *document_ids = new_document_ids; + } + + if is_sync_limited { + response.set_sync_token(Urn::Sync { id, seq: seq + 1 }.to_string()); + } + } + + if !is_sync_limited { + // Set sync token + let change_id = std::cmp::max( + container_changes.to_change_id, + children_changes.as_ref().map_or(0, |c| c.to_change_id), + ); + let sync_token = if change_id != 0 { + let sync_token = Urn::Sync { + id: change_id, + seq: 0, + } + .to_string(); + data.accounts.entry(account_id).or_default().sync_token = + sync_token.clone().into(); + sync_token + } else { + data.sync_token(self, account_id, collection_container) + .await + .caused_by(trc::location!())? + }; + + response.set_sync_token(sync_token); + } } SyncType::Initial => { response.set_sync_token( @@ -709,10 +754,6 @@ impl PropFindRequestHandler for Server { }; let view_as_id = access_token.primary_id(); - let mut limit = std::cmp::min( - query.limit.unwrap_or(u32::MAX) as usize, - self.core.groupware.max_results, - ); for item in paths { let account_id = item.account_id; let document_id = item.document_id; @@ -1332,7 +1373,7 @@ impl PropFindRequestHandler for Server { } } - if limit == 0 { + if limit == 0 || is_sync_limited { response.add_response( Response::new_status([query.uri], StatusCode::INSUFFICIENT_STORAGE) .with_error(BaseCondition::NumberOfMatchesWithinLimit) @@ -1449,7 +1490,7 @@ impl PropFindData { .await .caused_by(trc::location!())? .unwrap_or_default(); - data.sync_token = Urn::Sync(id).to_string().into(); + data.sync_token = Urn::Sync { id, seq: 0 }.to_string().into(); } Ok(data.sync_token.clone().unwrap()) diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index a0fd4042..e296e019 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -26,7 +26,7 @@ pub(crate) struct UriResource { pub(crate) enum Urn { Lock(u64), - Sync(u64), + Sync { id: u64, seq: u32 }, } pub(crate) type UnresolvedUri<'x> = UriResource, Option<&'x str>>; @@ -188,12 +188,28 @@ impl UriResource { } impl Urn { + pub fn try_extract_sync_id(token: &str) -> Option<&str> { + token + .strip_prefix("urn:stalwart:davsync:") + .map(|x| x.split_once(':').map(|(x, _)| x).unwrap_or(x)) + } + pub fn parse(input: &str) -> Option { let inbox = input.strip_prefix("urn:stalwart:")?; let (kind, id) = inbox.split_once(':')?; match kind { "davlock" => u64::from_str_radix(id, 16).ok().map(Urn::Lock), - "davsync" => u64::from_str_radix(id, 16).ok().map(Urn::Sync), + "davsync" => { + if let Some((id, seq)) = id.split_once(':') { + let id = u64::from_str_radix(id, 16).ok()?; + let seq = u32::from_str_radix(seq, 16).ok()?; + Some(Urn::Sync { id, seq }) + } else { + u64::from_str_radix(id, 16) + .ok() + .map(|id| Urn::Sync { id, seq: 0 }) + } + } _ => None, } } @@ -205,9 +221,9 @@ impl Urn { } } - pub fn try_unwrap_sync(&self) -> Option { + pub fn try_unwrap_sync(&self) -> Option<(u64, u32)> { match self { - Urn::Sync(id) => Some(*id), + Urn::Sync { id, seq } => Some((*id, *seq)), _ => None, } } @@ -217,7 +233,13 @@ impl Display for Urn { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Urn::Lock(id) => write!(f, "urn:stalwart:davlock:{id:x}",), - Urn::Sync(id) => write!(f, "urn:stalwart:davsync:{id:x}"), + Urn::Sync { id, seq } => { + if *seq == 0 { + write!(f, "urn:stalwart:davsync:{id:x}") + } else { + write!(f, "urn:stalwart:davsync:{id:x}:{seq:x}") + } + } } } } diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index f960a19b..5bf3a23c 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -192,7 +192,7 @@ impl PrincipalPropFind for Server { .unwrap_or_default(); fields.push(DavPropertyValue::new( property.clone(), - Urn::Sync(id).to_string(), + Urn::Sync { id, seq: 0 }.to_string(), )); } WebDavProperty::Owner => { diff --git a/tests/src/webdav/cal_query.rs b/tests/src/webdav/cal_query.rs index 223937bb..775e0122 100644 --- a/tests/src/webdav/cal_query.rs +++ b/tests/src/webdav/cal_query.rs @@ -19,7 +19,7 @@ use hyper::StatusCode; use store::write::serialize::rkyv_unarchive; pub async fn test(test: &WebDavTest) { - println!("Running REPORT calendar-query tests..."); + println!("Running REPORT calendar-query & free-busy-query tests..."); let client = test.client("john"); let cal_path = format!("{}/john/default/", DavResourceName::Cal.base_path()); @@ -732,8 +732,7 @@ BEGIN:VFREEBUSY DTSTART:20060104T140000Z DTEND:20060105T220000Z FREEBUSY;FBTYPE=BUSY-TENTATIVE:20060104T150000Z/20060104T160000Z -FREEBUSY;FBTYPE=BUSY:20060105T170000Z/20060105T180000Z -FREEBUSY;FBTYPE=BUSY:20060104T190000Z/20060104T200000Z +FREEBUSY;FBTYPE=BUSY:20060104T190000Z/20060104T200000Z;20060105T170000Z/20060105T180000Z FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20060105T100000Z/20060105T120000Z END:VFREEBUSY END:VCALENDAR @@ -753,11 +752,9 @@ BEGIN:VFREEBUSY DTSTART:20060101T000000Z DTEND:20060104T140000Z DTSTAMP:20250505T105255Z -FREEBUSY;FBTYPE=BUSY:20060102T150000Z/20060102T160000Z -FREEBUSY;FBTYPE=BUSY:20060102T170000Z/20060102T180000Z;20060103T170000Z/20060103T180000Z FREEBUSY;FBTYPE=BUSY-TENTATIVE:20060102T100000Z/20060102T120000Z -FREEBUSY:20060103T100000Z/20060103T120000Z -FREEBUSY:20060104T100000Z/20060104T120000Z +FREEBUSY;FBTYPE=BUSY:20060102T150000Z/20060102T160000Z;20060102T170000Z/20060102T180000Z; + 20060103T100000Z/20060103T120000Z;20060103T170000Z/20060103T180000Z;20060104T100000Z/20060104T120000Z END:VFREEBUSY END:VCALENDAR "#; diff --git a/tests/src/webdav/copy_move.rs b/tests/src/webdav/copy_move.rs index 885b9984..5fe21fb7 100644 --- a/tests/src/webdav/copy_move.rs +++ b/tests/src/webdav/copy_move.rs @@ -31,7 +31,7 @@ pub async fn test(test: &WebDavTest) { // Obtain sync token let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_eq!( response.hrefs().len(), @@ -54,6 +54,7 @@ pub async fn test(test: &WebDavTest) { &user_base_path, prev_sync_token, Depth::Infinity, + None, ["D:getetag"], ) .await; @@ -103,7 +104,7 @@ pub async fn test(test: &WebDavTest) { .await .with_status(StatusCode::CREATED); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; replace_prefix(&mut hierarchy, &hierarchy_root, &new_hierarchy_root); assert_result(&response, &hierarchy); @@ -122,7 +123,7 @@ pub async fn test(test: &WebDavTest) { .await .with_status(StatusCode::CREATED); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; let mut copied_hierarchy = hierarchy.clone(); replace_prefix(&mut copied_hierarchy, &hierarchy_root, &new_hierarchy_root); @@ -136,7 +137,7 @@ pub async fn test(test: &WebDavTest) { .await .with_status(StatusCode::NO_CONTENT); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_result(&response, &hierarchy); client.validate_values(&hierarchy).await; @@ -154,7 +155,7 @@ pub async fn test(test: &WebDavTest) { .await .with_status(StatusCode::NO_CONTENT); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; replace_prefix(&mut hierarchy, &new_hierarchy_root, &hierarchy_root); assert_result(&response, &hierarchy); @@ -174,7 +175,7 @@ pub async fn test(test: &WebDavTest) { .await .with_status(StatusCode::NO_CONTENT); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; let mut orig_hierarchy = new_hierarchy.clone(); replace_prefix(&mut orig_hierarchy, &new_hierarchy_root, &hierarchy_root); @@ -205,12 +206,12 @@ pub async fn test(test: &WebDavTest) { .await .with_status(StatusCode::CREATED); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_result(&response, &orig_hierarchy); client.validate_values(&orig_hierarchy).await; let response = client - .sync_collection(&group_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&group_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; replace_prefix( &mut full_hierarchy, @@ -260,7 +261,7 @@ pub async fn test(test: &WebDavTest) { hierarchy.push((folder_path, "".to_string())); } let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_result(&response, &hierarchy); client.validate_values(&hierarchy).await; @@ -316,7 +317,7 @@ pub async fn test(test: &WebDavTest) { .with_status(StatusCode::CREATED); rename(&mut hierarchy, &folder1_file1, &folder1_file1_new); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_result(&response, &hierarchy); client.validate_values(&hierarchy).await; @@ -338,7 +339,7 @@ pub async fn test(test: &WebDavTest) { &folder2_file1_from_folder1, ); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_result(&response, &hierarchy); client.validate_values(&hierarchy).await; @@ -357,7 +358,7 @@ pub async fn test(test: &WebDavTest) { delete(&mut hierarchy, &folder1_file2); rename(&mut hierarchy, &folder2_file1_from_folder1, &folder1_file2); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_result(&response, &hierarchy); client.validate_values(&hierarchy).await; @@ -376,7 +377,7 @@ pub async fn test(test: &WebDavTest) { .with_status(StatusCode::CREATED); copy(&mut hierarchy, &file3_path, &folder3_file3_from_folder1); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_result(&response, &hierarchy); client.validate_values(&hierarchy).await; @@ -395,7 +396,7 @@ pub async fn test(test: &WebDavTest) { delete(&mut hierarchy, &folder2_file2); copy(&mut hierarchy, &folder3_file3_from_folder1, &folder2_file2); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_result(&response, &hierarchy); client.validate_values(&hierarchy).await; @@ -440,12 +441,12 @@ pub async fn test(test: &WebDavTest) { ]; delete(&mut hierarchy, &folder3_file1); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_result(&response, &hierarchy); client.validate_values(&hierarchy).await; let response = client - .sync_collection(&group_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&group_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_result(&response, &shared_hierarchy); client.validate_values(&shared_hierarchy).await; @@ -470,7 +471,7 @@ pub async fn test(test: &WebDavTest) { .with_status(StatusCode::CREATED); replace_prefix(&mut hierarchy, &folder3, &folder2_folder3); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_result(&response, &hierarchy); client.validate_values(&hierarchy).await; @@ -501,7 +502,7 @@ pub async fn test(test: &WebDavTest) { .await .with_status(StatusCode::CREATED); let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; copy_prefix(&mut hierarchy, &folder1, &folder2_folder1); assert_result(&response, &hierarchy); diff --git a/tests/src/webdav/mod.rs b/tests/src/webdav/mod.rs index 1c23877c..c6d1f265 100644 --- a/tests/src/webdav/mod.rs +++ b/tests/src/webdav/mod.rs @@ -21,10 +21,7 @@ use common::{ core::BuildServer, manager::boot::build_ipc, }; -use dav_proto::{ - Depth, - schema::property::{DavProperty, WebDavProperty}, -}; +use dav_proto::schema::property::{DavProperty, WebDavProperty}; use groupware::{DavResourceName, hierarchy::DavHierarchy}; use http::HttpSessionManager; use hyper::{HeaderMap, Method, StatusCode, header::AUTHORIZATION}; @@ -80,6 +77,7 @@ pub async fn webdav_tests() { principals::test(&handle).await; acl::test(&handle).await; card_query::test(&handle).await; + cal_query::test(&handle).await; // Print elapsed time let elapsed = start_time.elapsed(); @@ -336,39 +334,6 @@ impl DummyWebDavClient { } } - pub async fn sync_collection( - &self, - path: &str, - sync_token: &str, - depth: Depth, - properties: impl IntoIterator, - ) -> DavResponse { - let mut request = concat!( - "", - "", - "" - ) - .to_string(); - - for property in properties { - request.push_str(&format!("<{property}/>")); - } - - request.push_str(""); - request.push_str(sync_token); - request.push_str(""); - request.push_str(match depth { - Depth::One => "1", - Depth::Infinity => "infinite", - _ => "0", - }); - request.push_str(""); - - self.request("REPORT", path, &request) - .await - .with_status(StatusCode::MULTI_STATUS) - } - pub async fn available_quota(&self, path: &str) -> u64 { self.propfind( path, diff --git a/tests/src/webdav/sync.rs b/tests/src/webdav/sync.rs index 54c7d38f..3206cc3e 100644 --- a/tests/src/webdav/sync.rs +++ b/tests/src/webdav/sync.rs @@ -4,8 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::WebDavTest; +use super::{DavResponse, DummyWebDavClient, WebDavTest}; use crate::webdav::GenerateTestDavResource; +use ahash::AHashSet; use dav_proto::Depth; use groupware::DavResourceName; use hyper::StatusCode; @@ -22,11 +23,11 @@ pub async fn test(test: &WebDavTest) { "Running REPORT sync-collection tests ({})...", resource_type.base_path() ); - let user_base_path = format!("{}/john", resource_type.base_path()); + let user_base_path = format!("{}/john/", resource_type.base_path()); // Test 1: Initial sync let response = client - .sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"]) + .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_eq!( response.hrefs().len(), @@ -46,13 +47,14 @@ pub async fn test(test: &WebDavTest) { &user_base_path, &sync_token_1, Depth::Infinity, + None, ["D:getetag"], ) .await; assert_eq!(response.hrefs(), Vec::::new()); // Test 3: Create a collection and make sure it is synced - let new_collection = format!("{}/new-collection/", user_base_path); + let new_collection = format!("{}new-collection/", user_base_path); client .mkcol("MKCOL", &new_collection, [], []) .await @@ -62,6 +64,7 @@ pub async fn test(test: &WebDavTest) { &user_base_path, &sync_token_1, Depth::Infinity, + None, ["D:getetag"], ) .await; @@ -80,6 +83,7 @@ pub async fn test(test: &WebDavTest) { &user_base_path, &sync_token_1, Depth::Infinity, + None, ["D:getetag"], ) .await; @@ -93,6 +97,7 @@ pub async fn test(test: &WebDavTest) { &user_base_path, &sync_token_2, Depth::Infinity, + None, ["D:getetag"], ) .await; @@ -100,13 +105,25 @@ pub async fn test(test: &WebDavTest) { // Test 5: sync-token with Depth 1 let response = client - .sync_collection(&user_base_path, &sync_token_1, Depth::One, ["D:getetag"]) + .sync_collection( + &user_base_path, + &sync_token_1, + Depth::One, + None, + ["D:getetag"], + ) .await; assert_eq!(response.hrefs(), vec![new_collection.clone()]); // Test 6: sync-token with Depth 0 let response = client - .sync_collection(&new_collection, &sync_token_1, Depth::Zero, ["D:getetag"]) + .sync_collection( + &new_collection, + &sync_token_1, + Depth::Zero, + None, + ["D:getetag"], + ) .await; assert_eq!(response.hrefs(), vec![new_collection.clone()]); @@ -138,6 +155,79 @@ pub async fn test(test: &WebDavTest) { .with_status(StatusCode::CREATED) .with_empty_body(); + // Test 9: Limit + let mut sync_token = client + .sync_collection( + &new_collection, + &sync_token_3, + Depth::Zero, + None, + ["D:getetag"], + ) + .await + .sync_token() + .to_string(); + let (folder_name, files) = client + .create_hierarchy(user_base_path.trim_end_matches('/'), 1, 0, 10) + .await; + let mut expected_changes = files + .iter() + .map(|x| x.0.as_str()) + .chain([folder_name.as_str()]) + .collect::>(); + for _ in 0..10 { + let response = client + .sync_collection( + &user_base_path, + &sync_token, + Depth::Infinity, + 2.into(), + ["D:getetag"], + ) + .await; + sync_token = response.sync_token().to_string(); + let hrefs = response.hrefs(); + if hrefs.is_empty() { + break; + } + let mut has_user_base_path = false; + let mut item_count = 0; + for href in hrefs { + if href == user_base_path { + has_user_base_path = true; + } else if expected_changes.remove(href) { + item_count += 1; + } else { + panic!("Unexpected href: {href}"); + } + } + if has_user_base_path { + assert_eq!(item_count, 2); + response + .with_value( + "D:multistatus.D:response.D:status", + "HTTP/1.1 507 Insufficient Storage", + ) + .with_value( + "D:multistatus.D:response.D:error.D:number-of-matches-within-limits", + "", + ) + .with_value( + "D:multistatus.D:response.D:responsedescription", + "The number of matches exceeds the limit of 2", + ); + } else { + assert!(item_count <= 2); + break; + } + } + assert!(expected_changes.is_empty(), "{:?}", expected_changes); + + client + .request("DELETE", &folder_name, "") + .await + .with_status(StatusCode::NO_CONTENT); + client .request("DELETE", &new_collection, "") .await @@ -147,3 +237,47 @@ pub async fn test(test: &WebDavTest) { client.delete_default_containers().await; test.assert_is_empty().await; } + +impl DummyWebDavClient { + pub async fn sync_collection( + &self, + path: &str, + sync_token: &str, + depth: Depth, + limit: Option, + properties: impl IntoIterator, + ) -> DavResponse { + let mut request = concat!( + "", + "", + "" + ) + .to_string(); + + for property in properties { + request.push_str(&format!("<{property}/>")); + } + + request.push_str(""); + request.push_str(sync_token); + request.push_str(""); + request.push_str(match depth { + Depth::One => "1", + Depth::Infinity => "infinite", + _ => "0", + }); + request.push_str(""); + + if let Some(limit) = limit { + request.push_str(""); + request.push_str(&limit.to_string()); + request.push_str(""); + } + + request.push_str(""); + + self.request("REPORT", path, &request) + .await + .with_status(StatusCode::MULTI_STATUS) + } +}