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:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+404
View File
@@ -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(),
},
}
}
+281
View File
@@ -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"));
}
}
+653
View File
@@ -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!(),
})
}