Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
+404
@@ -0,0 +1,404 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::GroupwareCache;
|
||||
use crate::{
|
||||
DavResourceName, RFC_3986,
|
||||
calendar::{
|
||||
ArchivedCalendar, ArchivedCalendarEvent, Calendar, CalendarEvent, SCHEDULE_INBOX_ID,
|
||||
SCHEDULE_OUTBOX_ID, storage::ItipAutoExpunge,
|
||||
},
|
||||
contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard},
|
||||
encode_path_segment,
|
||||
};
|
||||
use calcard::common::timezone::Tz;
|
||||
use common::{
|
||||
DavName, DavPath, DavResource, DavResourceMetadata, DavResources, Server,
|
||||
TinyCalendarPreferences, UpdateLock,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use store::ahash::{AHashMap, AHashSet};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::AclGrant,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
pub(super) async fn build_calcard_resources(
|
||||
server: &Server,
|
||||
access_account_id: u32,
|
||||
account_id: u32,
|
||||
sync_collection: SyncCollection,
|
||||
container_collection: Collection,
|
||||
item_collection: Collection,
|
||||
update_lock: Arc<UpdateLock>,
|
||||
) -> trc::Result<DavResources> {
|
||||
let is_calendar = matches!(sync_collection, SyncCollection::Calendar);
|
||||
let owner_account_info = server.account(account_id).await?;
|
||||
let access_account_info = if account_id == access_account_id {
|
||||
owner_account_info.clone()
|
||||
} else {
|
||||
server.account(access_account_id).await?
|
||||
};
|
||||
let mut cache = DavResources {
|
||||
base_path: format!(
|
||||
"{}/{}/",
|
||||
if is_calendar {
|
||||
DavResourceName::Cal
|
||||
} else {
|
||||
DavResourceName::Card
|
||||
}
|
||||
.base_path(),
|
||||
percent_encoding::utf8_percent_encode(owner_account_info.name(), RFC_3986),
|
||||
),
|
||||
paths: AHashSet::with_capacity(16),
|
||||
resources: Vec::with_capacity(16),
|
||||
item_change_id: 0,
|
||||
container_change_id: 0,
|
||||
highest_change_id: 0,
|
||||
size: std::mem::size_of::<DavResources>() as u64,
|
||||
update_lock,
|
||||
};
|
||||
|
||||
let mut is_first_check = true;
|
||||
loop {
|
||||
let last_change_id = server
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_last_change_id(account_id, sync_collection.into())
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap_or_default();
|
||||
cache.item_change_id = last_change_id;
|
||||
cache.container_change_id = last_change_id;
|
||||
cache.highest_change_id = last_change_id;
|
||||
cache.update_lock.set_revision(last_change_id);
|
||||
|
||||
server
|
||||
.archives(
|
||||
account_id,
|
||||
container_collection,
|
||||
&(),
|
||||
|document_id, archive| {
|
||||
let resource = if is_calendar {
|
||||
resource_from_calendar(archive.unarchive::<Calendar>()?, document_id)
|
||||
} else {
|
||||
resource_from_addressbook(archive.unarchive::<AddressBook>()?, document_id)
|
||||
};
|
||||
let path = DavPath {
|
||||
path: encode_path_segment(resource.container_name().unwrap()).into_owned(),
|
||||
parent_id: None,
|
||||
hierarchy_seq: 1,
|
||||
resource_idx: cache.resources.len(),
|
||||
};
|
||||
|
||||
cache.size += (std::mem::size_of::<DavPath>()
|
||||
+ std::mem::size_of::<DavResource>()
|
||||
+ (path.path.len()) * 2) as u64;
|
||||
cache.paths.insert(path);
|
||||
cache.resources.push(resource);
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if cache.paths.is_empty() {
|
||||
if is_first_check {
|
||||
if is_calendar {
|
||||
server
|
||||
.create_default_calendar(&access_account_info, &owner_account_info)
|
||||
.await?;
|
||||
} else {
|
||||
server
|
||||
.create_default_addressbook(&access_account_info, &owner_account_info)
|
||||
.await?;
|
||||
}
|
||||
is_first_check = false;
|
||||
continue;
|
||||
} else {
|
||||
return Ok(cache);
|
||||
}
|
||||
}
|
||||
|
||||
let parent_range = cache.resources.len();
|
||||
server
|
||||
.archives(account_id, item_collection, &(), |document_id, archive| {
|
||||
let resource = if is_calendar {
|
||||
resource_from_event(archive.unarchive::<CalendarEvent>()?, document_id)
|
||||
} else {
|
||||
resource_from_card(archive.unarchive::<ContactCard>()?, document_id)
|
||||
};
|
||||
let resource_idx = cache.resources.len();
|
||||
|
||||
for name in resource.child_names().unwrap_or_default().iter() {
|
||||
if let Some(parent) =
|
||||
cache.resources.get(..parent_range).and_then(|resources| {
|
||||
resources.iter().find(|r| r.document_id == name.parent_id)
|
||||
})
|
||||
{
|
||||
let path = DavPath {
|
||||
path: format!(
|
||||
"{}/{}",
|
||||
encode_path_segment(parent.container_name().unwrap()),
|
||||
encode_path_segment(&name.name)
|
||||
),
|
||||
parent_id: Some(name.parent_id),
|
||||
hierarchy_seq: 0,
|
||||
resource_idx,
|
||||
};
|
||||
|
||||
cache.size += (std::mem::size_of::<DavPath>()
|
||||
+ name.name.len()
|
||||
+ path.path.len()) as u64;
|
||||
cache.paths.insert(path);
|
||||
}
|
||||
}
|
||||
cache.size += std::mem::size_of::<DavResource>() as u64;
|
||||
cache.resources.push(resource);
|
||||
|
||||
Ok(true)
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
return Ok(cache);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn build_scheduling_resources(
|
||||
server: &Server,
|
||||
account_id: u32,
|
||||
update_lock: Arc<UpdateLock>,
|
||||
) -> trc::Result<DavResources> {
|
||||
let last_change_id = server
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_last_change_id(account_id, SyncCollection::CalendarEventNotification.into())
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap_or_default();
|
||||
|
||||
let account_info = server.account(account_id).await?;
|
||||
let item_ids = server
|
||||
.itip_ids(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
update_lock.set_revision(last_change_id);
|
||||
let mut cache = DavResources {
|
||||
base_path: format!(
|
||||
"{}/{}/",
|
||||
DavResourceName::Scheduling.base_path(),
|
||||
percent_encoding::utf8_percent_encode(account_info.name(), RFC_3986),
|
||||
),
|
||||
paths: AHashSet::with_capacity((2 + item_ids.len()) as usize),
|
||||
resources: Vec::with_capacity((2 + item_ids.len()) as usize),
|
||||
item_change_id: last_change_id,
|
||||
container_change_id: last_change_id,
|
||||
highest_change_id: last_change_id,
|
||||
size: std::mem::size_of::<DavResources>() as u64,
|
||||
update_lock,
|
||||
};
|
||||
|
||||
for (document_id, is_container) in item_ids
|
||||
.into_iter()
|
||||
.map(|document_id| (document_id, false))
|
||||
.chain([(SCHEDULE_INBOX_ID, true), (SCHEDULE_OUTBOX_ID, true)])
|
||||
{
|
||||
let path = path_from_scheduling(document_id, cache.resources.len(), is_container);
|
||||
cache.size += (std::mem::size_of::<DavPath>() + (path.path.len() * 2)) as u64
|
||||
+ std::mem::size_of::<DavResource>() as u64;
|
||||
cache.paths.insert(path);
|
||||
cache
|
||||
.resources
|
||||
.push(resource_from_scheduling(document_id, is_container));
|
||||
}
|
||||
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
pub(super) fn build_simple_hierarchy(cache: &mut DavResources) {
|
||||
cache.paths = AHashSet::with_capacity(cache.resources.len());
|
||||
let name_idx = cache
|
||||
.resources
|
||||
.iter()
|
||||
.filter_map(|resource| {
|
||||
resource
|
||||
.container_name()
|
||||
.map(|name| (resource.document_id, name))
|
||||
})
|
||||
.collect::<AHashMap<_, _>>();
|
||||
|
||||
for (resource_idx, resource) in cache.resources.iter().enumerate() {
|
||||
match &resource.data {
|
||||
DavResourceMetadata::Calendar { name, .. }
|
||||
| DavResourceMetadata::AddressBook { name, .. } => {
|
||||
let path = DavPath {
|
||||
path: encode_path_segment(name).into_owned(),
|
||||
parent_id: None,
|
||||
hierarchy_seq: 1,
|
||||
resource_idx,
|
||||
};
|
||||
cache.size +=
|
||||
(std::mem::size_of::<DavPath>() + name.len() + path.path.len()) as u64;
|
||||
cache.paths.insert(path);
|
||||
}
|
||||
DavResourceMetadata::CalendarEvent { names, .. }
|
||||
| DavResourceMetadata::ContactCard { names } => {
|
||||
for name in names {
|
||||
if let Some(parent_name) = name_idx.get(&name.parent_id) {
|
||||
let path = DavPath {
|
||||
path: format!(
|
||||
"{}/{}",
|
||||
encode_path_segment(parent_name),
|
||||
encode_path_segment(&name.name)
|
||||
),
|
||||
parent_id: Some(name.parent_id),
|
||||
hierarchy_seq: 0,
|
||||
resource_idx,
|
||||
};
|
||||
cache.size += (std::mem::size_of::<DavPath>()
|
||||
+ name.name.len()
|
||||
+ path.path.len()) as u64;
|
||||
cache.paths.insert(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
cache.size += std::mem::size_of::<DavResource>() as u64;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resource_from_calendar(calendar: &ArchivedCalendar, document_id: u32) -> DavResource {
|
||||
DavResource {
|
||||
document_id,
|
||||
data: DavResourceMetadata::Calendar {
|
||||
name: calendar.name.to_string(),
|
||||
acls: calendar
|
||||
.acls
|
||||
.iter()
|
||||
.map(|acl| AclGrant {
|
||||
account_id: acl.account_id.to_native(),
|
||||
grants: Bitmap::from(&acl.grants),
|
||||
})
|
||||
.collect(),
|
||||
preferences: calendar
|
||||
.preferences
|
||||
.iter()
|
||||
.map(|pref| TinyCalendarPreferences {
|
||||
account_id: pref.account_id.to_native(),
|
||||
flags: pref.flags.to_native(),
|
||||
tz: pref.time_zone.tz().unwrap_or(Tz::UTC),
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resource_from_event(event: &ArchivedCalendarEvent, document_id: u32) -> DavResource {
|
||||
let (start, duration) = event.data.event_range().unwrap_or_default();
|
||||
DavResource {
|
||||
document_id,
|
||||
data: DavResourceMetadata::CalendarEvent {
|
||||
names: event
|
||||
.names
|
||||
.iter()
|
||||
.map(|name| DavName {
|
||||
name: name.name.to_string(),
|
||||
parent_id: name.parent_id.to_native(),
|
||||
})
|
||||
.collect(),
|
||||
start,
|
||||
duration,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resource_from_scheduling(document_id: u32, is_container: bool) -> DavResource {
|
||||
DavResource {
|
||||
document_id,
|
||||
data: DavResourceMetadata::CalendarEventNotification {
|
||||
names: if !is_container {
|
||||
[DavName {
|
||||
name: format!("{document_id}.ics"),
|
||||
parent_id: SCHEDULE_INBOX_ID,
|
||||
}]
|
||||
.into_iter()
|
||||
.collect()
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn path_from_scheduling(
|
||||
document_id: u32,
|
||||
resource_idx: usize,
|
||||
is_container: bool,
|
||||
) -> DavPath {
|
||||
if is_container {
|
||||
DavPath {
|
||||
path: if document_id == SCHEDULE_INBOX_ID {
|
||||
"inbox".to_string()
|
||||
} else {
|
||||
"outbox".to_string()
|
||||
},
|
||||
parent_id: None,
|
||||
hierarchy_seq: 1,
|
||||
resource_idx,
|
||||
}
|
||||
} else {
|
||||
DavPath {
|
||||
path: format!("inbox/{document_id}.ics"),
|
||||
parent_id: Some(SCHEDULE_INBOX_ID),
|
||||
hierarchy_seq: 0,
|
||||
resource_idx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resource_from_addressbook(
|
||||
book: &ArchivedAddressBook,
|
||||
document_id: u32,
|
||||
) -> DavResource {
|
||||
DavResource {
|
||||
document_id,
|
||||
data: DavResourceMetadata::AddressBook {
|
||||
name: book.name.to_string(),
|
||||
acls: book
|
||||
.acls
|
||||
.iter()
|
||||
.map(|acl| AclGrant {
|
||||
account_id: acl.account_id.to_native(),
|
||||
grants: Bitmap::from(&acl.grants),
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resource_from_card(card: &ArchivedContactCard, document_id: u32) -> DavResource {
|
||||
DavResource {
|
||||
document_id,
|
||||
data: DavResourceMetadata::ContactCard {
|
||||
names: card
|
||||
.names
|
||||
.iter()
|
||||
.map(|name| DavName {
|
||||
name: name.name.to_string(),
|
||||
parent_id: name.parent_id.to_native(),
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
Vendored
+281
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavResourceName, RFC_3986, encode_path_segment,
|
||||
file::{ArchivedFileNode, FileNode},
|
||||
};
|
||||
use common::{DavPath, DavResource, DavResourceMetadata, DavResources, Server, UpdateLock};
|
||||
use std::sync::Arc;
|
||||
use store::ahash::{AHashMap, AHashSet};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::AclGrant,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
use utils::{map::bitmap::Bitmap, topological::TopologicalSort};
|
||||
|
||||
pub(super) async fn build_file_resources(
|
||||
server: &Server,
|
||||
account_id: u32,
|
||||
update_lock: Arc<UpdateLock>,
|
||||
) -> trc::Result<DavResources> {
|
||||
let last_change_id = server
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_last_change_id(account_id, SyncCollection::FileNode.into())
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap_or_default();
|
||||
let account_info = server.account(account_id).await?;
|
||||
|
||||
let mut resources = Vec::with_capacity(16);
|
||||
server
|
||||
.archives(
|
||||
account_id,
|
||||
Collection::FileNode,
|
||||
&(),
|
||||
|document_id, archive| {
|
||||
resources.push(resource_from_file(
|
||||
archive.unarchive::<FileNode>()?,
|
||||
document_id,
|
||||
));
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
update_lock.set_revision(last_change_id);
|
||||
let mut files = DavResources {
|
||||
base_path: format!(
|
||||
"{}/{}/",
|
||||
DavResourceName::File.base_path(),
|
||||
percent_encoding::utf8_percent_encode(account_info.name(), RFC_3986),
|
||||
),
|
||||
size: std::mem::size_of::<DavResources>() as u64,
|
||||
paths: AHashSet::with_capacity(resources.len()),
|
||||
resources,
|
||||
item_change_id: last_change_id,
|
||||
container_change_id: last_change_id,
|
||||
highest_change_id: last_change_id,
|
||||
update_lock,
|
||||
};
|
||||
|
||||
build_nested_hierarchy(&mut files);
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
pub(super) fn build_nested_hierarchy(resources: &mut DavResources) {
|
||||
let mut topological_sort = TopologicalSort::with_capacity(resources.resources.len());
|
||||
let mut names = AHashMap::with_capacity(resources.resources.len());
|
||||
|
||||
for (resource_idx, resource) in resources.resources.iter().enumerate() {
|
||||
if let DavResourceMetadata::File { parent_id, .. } = resource.data {
|
||||
topological_sort.insert(
|
||||
parent_id.map(|id| id + 1).unwrap_or_default(),
|
||||
resource.document_id + 1,
|
||||
);
|
||||
names.insert(
|
||||
resource.document_id,
|
||||
DavPath {
|
||||
path: encode_path_segment(resource.container_name().unwrap()).into_owned(),
|
||||
parent_id,
|
||||
hierarchy_seq: 0,
|
||||
resource_idx,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (hierarchy_sequence, folder_id) in topological_sort.into_iterator().enumerate() {
|
||||
if folder_id != 0 {
|
||||
let folder_id = folder_id - 1;
|
||||
let path = names
|
||||
.get(&folder_id)
|
||||
.and_then(|folder| folder.parent_id.map(|parent_id| (&folder.path, parent_id)))
|
||||
.and_then(|(name, parent_id)| {
|
||||
names
|
||||
.get(&parent_id)
|
||||
.map(|parent| format!("{}/{}", parent.path, name))
|
||||
});
|
||||
|
||||
if let Some(folder) = names.get_mut(&folder_id) {
|
||||
if let Some(path) = path {
|
||||
folder.path = path;
|
||||
}
|
||||
folder.hierarchy_seq = hierarchy_sequence as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resources.paths = names
|
||||
.into_values()
|
||||
.inspect(|v| {
|
||||
resources.size += (std::mem::size_of::<DavPath>()
|
||||
+ std::mem::size_of::<u32>()
|
||||
+ std::mem::size_of::<usize>()
|
||||
+ std::mem::size_of::<DavResource>()
|
||||
+ v.path.len()) as u64;
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub(super) fn resource_from_file(node: &ArchivedFileNode, document_id: u32) -> DavResource {
|
||||
let parent_id = node.parent_id.to_native();
|
||||
DavResource {
|
||||
document_id,
|
||||
data: DavResourceMetadata::File {
|
||||
name: node.name.as_str().to_string(),
|
||||
size: node.file.as_ref().map(|f| f.size.to_native()),
|
||||
parent_id: if parent_id > 0 {
|
||||
Some(parent_id - 1)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
acls: node
|
||||
.acls
|
||||
.iter()
|
||||
.map(|acl| AclGrant {
|
||||
account_id: acl.account_id.to_native(),
|
||||
grants: Bitmap::from(&acl.grants),
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const MISSING_DOCUMENT_ID: u32 = 9;
|
||||
|
||||
fn folder(document_id: u32, name: &str, parent_id: Option<u32>) -> DavResource {
|
||||
DavResource {
|
||||
document_id,
|
||||
data: DavResourceMetadata::File {
|
||||
name: name.to_string(),
|
||||
size: None,
|
||||
parent_id,
|
||||
acls: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn file(document_id: u32, name: &str, parent_id: Option<u32>) -> DavResource {
|
||||
DavResource {
|
||||
document_id,
|
||||
data: DavResourceMetadata::File {
|
||||
name: name.to_string(),
|
||||
size: Some(1024),
|
||||
parent_id,
|
||||
acls: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn build(resources: Vec<DavResource>) -> DavResources {
|
||||
let mut files = DavResources {
|
||||
base_path: "/dav/file/john/".to_string(),
|
||||
paths: AHashSet::with_capacity(resources.len()),
|
||||
resources,
|
||||
item_change_id: 0,
|
||||
container_change_id: 0,
|
||||
highest_change_id: 0,
|
||||
size: 0,
|
||||
update_lock: Arc::new(UpdateLock::new()),
|
||||
};
|
||||
build_nested_hierarchy(&mut files);
|
||||
files
|
||||
}
|
||||
|
||||
fn sorted_paths(files: &DavResources) -> Vec<&str> {
|
||||
let mut paths = files
|
||||
.paths
|
||||
.iter()
|
||||
.map(|path| path.path.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
paths.sort_unstable();
|
||||
paths
|
||||
}
|
||||
|
||||
fn hierarchy_seq(files: &DavResources, path: &str) -> u32 {
|
||||
files.paths.get(path).expect(path).hierarchy_seq
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_hierarchy() {
|
||||
let files = build(vec![
|
||||
folder(0, "docs", None),
|
||||
folder(1, "reports", Some(0)),
|
||||
file(2, "q1.txt", Some(1)),
|
||||
file(3, "readme.txt", None),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
sorted_paths(&files),
|
||||
["docs", "docs/reports", "docs/reports/q1.txt", "readme.txt"]
|
||||
);
|
||||
assert!(hierarchy_seq(&files, "docs") < hierarchy_seq(&files, "docs/reports"));
|
||||
assert!(
|
||||
hierarchy_seq(&files, "docs/reports") < hierarchy_seq(&files, "docs/reports/q1.txt")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_hierarchy_percent_encodes_paths() {
|
||||
let files = build(vec![
|
||||
folder(0, "My Documents", None),
|
||||
folder(1, "Berichte 2026", Some(0)),
|
||||
file(2, "Ünterlagen Q1.txt", Some(1)),
|
||||
folder(3, "My%20Folder", None),
|
||||
file(4, "file(1)+a:b.txt", Some(3)),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
sorted_paths(&files),
|
||||
[
|
||||
"My%20Documents",
|
||||
"My%20Documents/Berichte%202026",
|
||||
"My%20Documents/Berichte%202026/%C3%9Cnterlagen%20Q1.txt",
|
||||
"My%20Folder",
|
||||
"My%20Folder/file(1)+a:b.txt",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
files.format_resource(files.by_path("My%20Documents").unwrap()),
|
||||
"/dav/file/john/My%20Documents/"
|
||||
);
|
||||
assert_eq!(
|
||||
files.format_resource(
|
||||
files
|
||||
.by_path("My%20Documents/Berichte%202026/%C3%9Cnterlagen%20Q1.txt")
|
||||
.unwrap()
|
||||
),
|
||||
"/dav/file/john/My%20Documents/Berichte%202026/%C3%9Cnterlagen%20Q1.txt"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_hierarchy_with_dangling_parent() {
|
||||
let files = build(vec![
|
||||
folder(0, "docs", None),
|
||||
folder(1, "reports", Some(MISSING_DOCUMENT_ID)),
|
||||
file(2, "q1.txt", Some(1)),
|
||||
file(3, "orphan.txt", Some(MISSING_DOCUMENT_ID)),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
sorted_paths(&files),
|
||||
["docs", "orphan.txt", "reports", "reports/q1.txt"]
|
||||
);
|
||||
assert!(hierarchy_seq(&files, "reports") < hierarchy_seq(&files, "reports/q1.txt"));
|
||||
}
|
||||
}
|
||||
Vendored
+653
@@ -0,0 +1,653 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
cache::calcard::{build_scheduling_resources, path_from_scheduling, resource_from_scheduling},
|
||||
calendar::{CALENDAR_SUBSCRIBED, Calendar, CalendarEvent, CalendarPreferences},
|
||||
contact::{AddressBook, AddressBookPreferences, ContactCard},
|
||||
file::FileNode,
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use calcard::{
|
||||
build_calcard_resources, build_simple_hierarchy, resource_from_addressbook,
|
||||
resource_from_calendar, resource_from_card, resource_from_event,
|
||||
};
|
||||
use common::{
|
||||
DavResource, DavResources, Server, UpdateLock, auth::AccountCache, cache::LockResult,
|
||||
};
|
||||
use file::{build_file_resources, build_nested_hierarchy, resource_from_file};
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use store::{
|
||||
SerializeInfallible, ValueKey,
|
||||
ahash::AHashMap,
|
||||
query::log::{Change, Query},
|
||||
write::{AlignedBytes, Archive, BatchBuilder, ValueClass},
|
||||
};
|
||||
use trc::{AddContext, StoreEvent};
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
field::PrincipalField,
|
||||
};
|
||||
use utils::cache::Cache;
|
||||
|
||||
pub mod calcard;
|
||||
pub mod file;
|
||||
|
||||
pub trait GroupwareCache: Sync + Send {
|
||||
fn fetch_dav_resources(
|
||||
&self,
|
||||
access_account_id: u32,
|
||||
account_id: u32,
|
||||
collection: SyncCollection,
|
||||
) -> impl Future<Output = trc::Result<Arc<DavResources>>> + Send;
|
||||
|
||||
fn create_default_addressbook(
|
||||
&self,
|
||||
account_info_access: &AccountCache,
|
||||
account_info_owner: &AccountCache,
|
||||
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
||||
|
||||
fn create_default_calendar(
|
||||
&self,
|
||||
account_info_access: &AccountCache,
|
||||
account_info_owner: &AccountCache,
|
||||
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
||||
|
||||
fn get_or_create_default_calendar(
|
||||
&self,
|
||||
access_account_id: u32,
|
||||
account_id: u32,
|
||||
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
||||
|
||||
fn cached_dav_resources(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: SyncCollection,
|
||||
) -> Option<Arc<DavResources>>;
|
||||
}
|
||||
|
||||
impl GroupwareCache for Server {
|
||||
async fn fetch_dav_resources(
|
||||
&self,
|
||||
access_account_id: u32,
|
||||
account_id: u32,
|
||||
collection: SyncCollection,
|
||||
) -> trc::Result<Arc<DavResources>> {
|
||||
let cache_store = match collection {
|
||||
SyncCollection::Calendar => &self.inner.cache.events,
|
||||
SyncCollection::AddressBook => &self.inner.cache.contacts,
|
||||
SyncCollection::FileNode => &self.inner.cache.files,
|
||||
SyncCollection::CalendarEventNotification => &self.inner.cache.scheduling,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut cache = match cache_store.get_value_or_guard_async(&account_id).await {
|
||||
Ok(cache) => cache,
|
||||
Err(guard) => {
|
||||
let start_time = Instant::now();
|
||||
let cache = full_cache_build(
|
||||
self,
|
||||
account_id,
|
||||
collection,
|
||||
Arc::new(UpdateLock::new()),
|
||||
access_account_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if guard.insert(cache.clone()).is_err() {
|
||||
cache_store.update(account_id, cache.clone());
|
||||
}
|
||||
warn_if_uncacheable(cache_store, account_id, collection, &cache);
|
||||
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheMiss),
|
||||
AccountId = account_id,
|
||||
Collection = collection.as_str(),
|
||||
Total = cache.resources.len(),
|
||||
ChangeId = cache.highest_change_id,
|
||||
Elapsed = start_time.elapsed(),
|
||||
);
|
||||
|
||||
return Ok(cache);
|
||||
}
|
||||
};
|
||||
|
||||
// Obtain current state
|
||||
let start_time = Instant::now();
|
||||
let changes = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.changes(
|
||||
account_id,
|
||||
collection.into(),
|
||||
Query::Since(cache.highest_change_id),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Regenerate cache if the change log has been truncated
|
||||
if changes.is_truncated {
|
||||
let cache = full_cache_build(
|
||||
self,
|
||||
account_id,
|
||||
collection,
|
||||
cache.update_lock.clone(),
|
||||
access_account_id,
|
||||
)
|
||||
.await?;
|
||||
cache_store.update(account_id, cache.clone());
|
||||
warn_if_uncacheable(cache_store, account_id, collection, &cache);
|
||||
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheStale),
|
||||
AccountId = account_id,
|
||||
Collection = collection.as_str(),
|
||||
ChangeId = cache.highest_change_id,
|
||||
Total = cache.resources.len(),
|
||||
Elapsed = start_time.elapsed(),
|
||||
);
|
||||
|
||||
return Ok(cache);
|
||||
}
|
||||
|
||||
// Verify changes
|
||||
if changes.changes.is_empty() {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheHit),
|
||||
AccountId = account_id,
|
||||
Collection = collection.as_str(),
|
||||
ChangeId = cache.highest_change_id,
|
||||
Elapsed = start_time.elapsed(),
|
||||
);
|
||||
|
||||
return Ok(cache);
|
||||
}
|
||||
|
||||
// Lock for updates
|
||||
let lock = cache.update_lock.clone();
|
||||
let _permit = match lock.acquire(cache.highest_change_id).await? {
|
||||
LockResult::Acquired(permit) => permit,
|
||||
LockResult::Stale(permit) => {
|
||||
cache = cache_store.peek(&account_id).unwrap_or(cache.clone());
|
||||
if cache.highest_change_id >= changes.to_change_id {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheHit),
|
||||
AccountId = account_id,
|
||||
Collection = collection.as_str(),
|
||||
ChangeId = cache.highest_change_id,
|
||||
Elapsed = start_time.elapsed(),
|
||||
);
|
||||
return Ok(cache);
|
||||
}
|
||||
|
||||
permit
|
||||
}
|
||||
};
|
||||
|
||||
let num_changes = changes.changes.len();
|
||||
let cache = if !matches!(collection, SyncCollection::CalendarEventNotification) {
|
||||
let mut updated_resources = AHashMap::with_capacity(8);
|
||||
let has_no_children = collection == SyncCollection::FileNode;
|
||||
|
||||
process_changes(
|
||||
self,
|
||||
account_id,
|
||||
collection,
|
||||
has_no_children,
|
||||
&mut updated_resources,
|
||||
changes.changes,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut rebuild_hierarchy = false;
|
||||
let mut resources = Vec::with_capacity(cache.resources.len());
|
||||
|
||||
for resource in &cache.resources {
|
||||
let is_container = has_no_children || resource.is_container();
|
||||
if let Some(updated_resource) =
|
||||
updated_resources.remove(&(is_container, resource.document_id))
|
||||
{
|
||||
if let Some(updated_resource) = updated_resource {
|
||||
rebuild_hierarchy =
|
||||
rebuild_hierarchy || updated_resource.has_hierarchy_changes(resource);
|
||||
resources.push(updated_resource);
|
||||
} else {
|
||||
// Deleted resource
|
||||
rebuild_hierarchy = true;
|
||||
}
|
||||
} else {
|
||||
resources.push(resource.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Add new resources
|
||||
for resource in updated_resources.into_values().flatten() {
|
||||
resources.push(resource);
|
||||
rebuild_hierarchy = true;
|
||||
}
|
||||
|
||||
if rebuild_hierarchy {
|
||||
let mut cache = DavResources {
|
||||
base_path: cache.base_path.clone(),
|
||||
paths: Default::default(),
|
||||
resources,
|
||||
item_change_id: changes.item_change_id.unwrap_or(cache.item_change_id),
|
||||
container_change_id: changes
|
||||
.container_change_id
|
||||
.unwrap_or(cache.container_change_id),
|
||||
highest_change_id: changes.to_change_id,
|
||||
size: std::mem::size_of::<DavResources>() as u64,
|
||||
update_lock: lock.clone(),
|
||||
};
|
||||
|
||||
if matches!(collection, SyncCollection::FileNode) {
|
||||
build_nested_hierarchy(&mut cache);
|
||||
} else {
|
||||
build_simple_hierarchy(&mut cache);
|
||||
}
|
||||
cache
|
||||
} else {
|
||||
DavResources {
|
||||
base_path: cache.base_path.clone(),
|
||||
paths: cache.paths.clone(),
|
||||
resources,
|
||||
item_change_id: changes.item_change_id.unwrap_or(cache.item_change_id),
|
||||
container_change_id: changes
|
||||
.container_change_id
|
||||
.unwrap_or(cache.container_change_id),
|
||||
highest_change_id: changes.to_change_id,
|
||||
size: cache.size,
|
||||
update_lock: lock.clone(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut delete_ids = AHashSet::with_capacity(changes.changes.len());
|
||||
let mut resources = Vec::with_capacity(cache.resources.len());
|
||||
let mut paths = AHashSet::with_capacity(cache.paths.len());
|
||||
|
||||
for change in changes.changes {
|
||||
match change {
|
||||
Change::InsertItem(document_id) => {
|
||||
let document_id = document_id as u32;
|
||||
paths.insert(path_from_scheduling(document_id, resources.len(), false));
|
||||
resources.push(resource_from_scheduling(document_id, false));
|
||||
}
|
||||
Change::DeleteItem(document_id) => {
|
||||
delete_ids.insert(document_id as u32);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for resource in &cache.resources {
|
||||
if !delete_ids.contains(&resource.document_id) {
|
||||
paths.insert(path_from_scheduling(
|
||||
resource.document_id,
|
||||
resources.len(),
|
||||
resource.is_container(),
|
||||
));
|
||||
resources.push(resource.clone());
|
||||
}
|
||||
}
|
||||
|
||||
DavResources {
|
||||
base_path: cache.base_path.clone(),
|
||||
paths,
|
||||
resources,
|
||||
item_change_id: changes.item_change_id.unwrap_or(cache.item_change_id),
|
||||
container_change_id: changes
|
||||
.container_change_id
|
||||
.unwrap_or(cache.container_change_id),
|
||||
highest_change_id: changes.to_change_id,
|
||||
size: cache.size,
|
||||
update_lock: cache.update_lock.clone(),
|
||||
}
|
||||
};
|
||||
|
||||
cache.update_lock.set_revision(cache.highest_change_id);
|
||||
let cache = Arc::new(cache);
|
||||
cache_store.update(account_id, cache.clone());
|
||||
warn_if_uncacheable(cache_store, account_id, collection, &cache);
|
||||
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheUpdate),
|
||||
AccountId = account_id,
|
||||
Collection = collection.as_str(),
|
||||
ChangeId = cache.highest_change_id,
|
||||
Details = num_changes,
|
||||
Total = cache.resources.len(),
|
||||
Elapsed = start_time.elapsed(),
|
||||
);
|
||||
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
async fn create_default_addressbook(
|
||||
&self,
|
||||
account_info_access: &AccountCache,
|
||||
account_info_owner: &AccountCache,
|
||||
) -> trc::Result<Option<u32>> {
|
||||
if let Some(name) = &self.core.groupware.default_addressbook_name {
|
||||
let mut batch = BatchBuilder::new();
|
||||
let account_id = account_info_owner.account_id();
|
||||
let account_name = account_info_owner.name();
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::AddressBook, 1)
|
||||
.await?;
|
||||
AddressBook {
|
||||
name: name.clone(),
|
||||
preferences: vec![AddressBookPreferences {
|
||||
account_id,
|
||||
name: format!(
|
||||
"{} ({})",
|
||||
self.core
|
||||
.groupware
|
||||
.default_addressbook_display_name
|
||||
.as_ref()
|
||||
.unwrap_or(name),
|
||||
account_name
|
||||
),
|
||||
..Default::default()
|
||||
}],
|
||||
subscribers: vec![account_id],
|
||||
..Default::default()
|
||||
}
|
||||
.insert(
|
||||
account_info_access.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)?;
|
||||
|
||||
batch
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.set(
|
||||
PrincipalField::DefaultAddressBookId,
|
||||
document_id.serialize(),
|
||||
);
|
||||
|
||||
self.commit_batch(batch).await?;
|
||||
Ok(Some(document_id))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_default_calendar(
|
||||
&self,
|
||||
account_info_access: &AccountCache,
|
||||
account_info_owner: &AccountCache,
|
||||
) -> trc::Result<Option<u32>> {
|
||||
if let Some(name) = &self.core.groupware.default_calendar_name {
|
||||
let mut batch = BatchBuilder::new();
|
||||
let account_id = account_info_owner.account_id();
|
||||
let account_name = account_info_owner.name();
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::Calendar, 1)
|
||||
.await?;
|
||||
Calendar {
|
||||
name: name.clone(),
|
||||
preferences: vec![CalendarPreferences {
|
||||
account_id,
|
||||
name: format!(
|
||||
"{} ({})",
|
||||
self.core
|
||||
.groupware
|
||||
.default_calendar_display_name
|
||||
.as_ref()
|
||||
.unwrap_or(name),
|
||||
account_name
|
||||
),
|
||||
flags: CALENDAR_SUBSCRIBED,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
.insert(
|
||||
account_info_access.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)?;
|
||||
|
||||
// Set default calendar
|
||||
batch
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.set(PrincipalField::DefaultCalendarId, document_id.serialize());
|
||||
|
||||
self.commit_batch(batch).await?;
|
||||
Ok(Some(document_id))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_or_create_default_calendar(
|
||||
&self,
|
||||
access_account_id: u32,
|
||||
account_id: u32,
|
||||
) -> trc::Result<Option<u32>> {
|
||||
let default_calendar_id = self
|
||||
.store()
|
||||
.get_value::<u32>(ValueKey {
|
||||
account_id,
|
||||
collection: Collection::Principal.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::Property(PrincipalField::DefaultCalendarId.into()),
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if default_calendar_id.is_some() {
|
||||
Ok(default_calendar_id)
|
||||
} else {
|
||||
self.fetch_dav_resources(access_account_id, account_id, SyncCollection::Calendar)
|
||||
.await
|
||||
.map(|c| c.document_ids(true).next())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn cached_dav_resources(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: SyncCollection,
|
||||
) -> Option<Arc<DavResources>> {
|
||||
(match collection {
|
||||
SyncCollection::Calendar => &self.inner.cache.events,
|
||||
SyncCollection::AddressBook => &self.inner.cache.contacts,
|
||||
SyncCollection::FileNode => &self.inner.cache.files,
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.get(&account_id)
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_changes(
|
||||
server: &Server,
|
||||
account_id: u32,
|
||||
collection: SyncCollection,
|
||||
has_no_children: bool,
|
||||
updated_resources: &mut AHashMap<(bool, u32), Option<DavResource>>,
|
||||
changes: Vec<Change>,
|
||||
) -> trc::Result<()> {
|
||||
for change in changes {
|
||||
match change {
|
||||
Change::InsertItem(id) | Change::UpdateItem(id) => {
|
||||
let document_id = id as u32;
|
||||
if let Some(archive) = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
collection.collection(false),
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
updated_resources.insert(
|
||||
(has_no_children, document_id),
|
||||
Some(resource_from_archive(
|
||||
archive,
|
||||
document_id,
|
||||
collection,
|
||||
false,
|
||||
)?),
|
||||
);
|
||||
} else {
|
||||
updated_resources.insert((has_no_children, document_id), None);
|
||||
}
|
||||
}
|
||||
Change::DeleteItem(id) => {
|
||||
updated_resources.insert((has_no_children, id as u32), None);
|
||||
}
|
||||
Change::InsertContainer(id) | Change::UpdateContainer(id) => {
|
||||
let document_id = id as u32;
|
||||
if let Some(archive) = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
collection.collection(true),
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
updated_resources.insert(
|
||||
(true, document_id),
|
||||
Some(resource_from_archive(
|
||||
archive,
|
||||
document_id,
|
||||
collection,
|
||||
true,
|
||||
)?),
|
||||
);
|
||||
} else {
|
||||
updated_resources.insert((true, document_id), None);
|
||||
}
|
||||
}
|
||||
Change::DeleteContainer(id) => {
|
||||
updated_resources.insert((true, id as u32), None);
|
||||
}
|
||||
Change::UpdateContainerProperty(_) => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn warn_if_uncacheable(
|
||||
cache_store: &Cache<u32, Arc<DavResources>>,
|
||||
account_id: u32,
|
||||
collection: SyncCollection,
|
||||
cache: &Arc<DavResources>,
|
||||
) {
|
||||
let capacity = cache_store.weight_capacity();
|
||||
if cache.size > capacity {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheEntryTooLarge),
|
||||
AccountId = account_id,
|
||||
Collection = collection.as_str(),
|
||||
Size = cache.size,
|
||||
Limit = capacity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn full_cache_build(
|
||||
server: &Server,
|
||||
account_id: u32,
|
||||
collection: SyncCollection,
|
||||
update_lock: Arc<UpdateLock>,
|
||||
access_account_id: u32,
|
||||
) -> trc::Result<Arc<DavResources>> {
|
||||
match collection {
|
||||
SyncCollection::Calendar => {
|
||||
build_calcard_resources(
|
||||
server,
|
||||
access_account_id,
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
Collection::Calendar,
|
||||
Collection::CalendarEvent,
|
||||
update_lock,
|
||||
)
|
||||
.await
|
||||
}
|
||||
SyncCollection::AddressBook => {
|
||||
build_calcard_resources(
|
||||
server,
|
||||
access_account_id,
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
Collection::AddressBook,
|
||||
Collection::ContactCard,
|
||||
update_lock,
|
||||
)
|
||||
.await
|
||||
}
|
||||
SyncCollection::FileNode => build_file_resources(server, account_id, update_lock).await,
|
||||
SyncCollection::CalendarEventNotification => {
|
||||
build_scheduling_resources(server, account_id, update_lock).await
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
.map(Arc::new)
|
||||
}
|
||||
|
||||
fn resource_from_archive(
|
||||
archive: Archive<AlignedBytes>,
|
||||
document_id: u32,
|
||||
collection: SyncCollection,
|
||||
is_container: bool,
|
||||
) -> trc::Result<DavResource> {
|
||||
Ok(match collection {
|
||||
SyncCollection::Calendar => {
|
||||
if is_container {
|
||||
resource_from_calendar(
|
||||
archive
|
||||
.unarchive::<Calendar>()
|
||||
.caused_by(trc::location!())?,
|
||||
document_id,
|
||||
)
|
||||
} else {
|
||||
resource_from_event(
|
||||
archive
|
||||
.unarchive::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?,
|
||||
document_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
SyncCollection::AddressBook => {
|
||||
if is_container {
|
||||
resource_from_addressbook(
|
||||
archive
|
||||
.unarchive::<AddressBook>()
|
||||
.caused_by(trc::location!())?,
|
||||
document_id,
|
||||
)
|
||||
} else {
|
||||
resource_from_card(
|
||||
archive
|
||||
.unarchive::<ContactCard>()
|
||||
.caused_by(trc::location!())?,
|
||||
document_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
SyncCollection::FileNode => resource_from_file(
|
||||
archive
|
||||
.unarchive::<FileNode>()
|
||||
.caused_by(trc::location!())?,
|
||||
document_id,
|
||||
),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user