From fe7d6469668db0e1bd79b40f3b47cda2f4e7c17f Mon Sep 17 00:00:00 2001 From: mdecimus Date: Thu, 8 May 2025 17:30:57 +0200 Subject: [PATCH] Groupware caching improvements --- crates/common/src/config/inner.rs | 18 +- crates/common/src/lib.rs | 306 ++++++++++++---- crates/common/src/sharing/mod.rs | 4 +- crates/common/src/sharing/resources.rs | 83 +++++ crates/common/src/storage/folder.rs | 217 ------------ crates/common/src/storage/mod.rs | 1 - crates/dav/src/calendar/copy_move.rs | 215 +++++------ crates/dav/src/calendar/delete.rs | 31 +- crates/dav/src/calendar/freebusy.rs | 35 +- crates/dav/src/calendar/get.rs | 35 +- crates/dav/src/calendar/mkcol.rs | 13 +- crates/dav/src/calendar/mod.rs | 7 +- crates/dav/src/calendar/proppatch.rs | 29 +- crates/dav/src/calendar/query.rs | 30 +- crates/dav/src/calendar/update.rs | 48 +-- crates/dav/src/card/copy_move.rs | 215 +++++------ crates/dav/src/card/delete.rs | 33 +- crates/dav/src/card/get.rs | 31 +- crates/dav/src/card/mkcol.rs | 10 +- crates/dav/src/card/mod.rs | 7 +- crates/dav/src/card/proppatch.rs | 29 +- crates/dav/src/card/query.rs | 26 +- crates/dav/src/card/update.rs | 47 +-- crates/dav/src/common/acl.rs | 133 +++---- crates/dav/src/common/lock.rs | 17 +- crates/dav/src/common/propfind.rs | 335 +++++++++-------- crates/dav/src/common/uri.rs | 10 +- crates/dav/src/file/copy_move.rs | 175 +++++---- crates/dav/src/file/delete.rs | 38 +- crates/dav/src/file/get.rs | 6 +- crates/dav/src/file/mkcol.rs | 52 ++- crates/dav/src/file/mod.rs | 29 +- crates/dav/src/file/proppatch.rs | 9 +- crates/dav/src/file/update.rs | 64 ++-- crates/dav/src/principal/propfind.rs | 17 +- crates/dav/src/request.rs | 46 ++- crates/email/src/mailbox/index.rs | 23 +- crates/groupware/Cargo.toml | 1 + crates/groupware/src/cache/calcard.rs | 300 ++++++++++++++++ crates/groupware/src/cache/file.rs | 182 ++++++++++ crates/groupware/src/cache/mod.rs | 414 ++++++++++++++++++++++ crates/groupware/src/calendar/dates.rs | 1 - crates/groupware/src/calendar/index.rs | 81 +---- crates/groupware/src/calendar/mod.rs | 2 +- crates/groupware/src/contact/index.rs | 37 +- crates/groupware/src/contact/mod.rs | 4 +- crates/groupware/src/file/index.rs | 96 ++--- crates/groupware/src/hierarchy.rs | 372 ------------------- crates/groupware/src/lib.rs | 76 +--- crates/jmap-proto/src/types/collection.rs | 55 +++ tests/src/webdav/basic.rs | 17 +- tests/src/webdav/mod.rs | 14 +- tests/src/webdav/principals.rs | 1 + tests/src/webdav/put_get.rs | 24 +- 54 files changed, 2138 insertions(+), 1963 deletions(-) create mode 100644 crates/common/src/sharing/resources.rs delete mode 100644 crates/common/src/storage/folder.rs create mode 100644 crates/groupware/src/cache/calcard.rs create mode 100644 crates/groupware/src/cache/file.rs create mode 100644 crates/groupware/src/cache/mod.rs delete mode 100644 crates/groupware/src/hierarchy.rs diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs index 76a6e96e..ce4f8177 100644 --- a/crates/common/src/config/inner.rs +++ b/crates/common/src/config/inner.rs @@ -116,9 +116,23 @@ impl Caches { + (1024 * std::mem::size_of::()) + (15 * (std::mem::size_of::() + 60))) as u64, ), - dav: Cache::from_config( + files: Cache::from_config( config, - "dav", + "files", + MB_10, + (std::mem::size_of::() + (500 * std::mem::size_of::())) + as u64, + ), + events: Cache::from_config( + config, + "events", + MB_10, + (std::mem::size_of::() + (500 * std::mem::size_of::())) + as u64, + ), + contacts: Cache::from_config( + config, + "contacts", MB_10, (std::mem::size_of::() + (500 * std::mem::size_of::())) as u64, diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index f98fabbf..ddfbcaf1 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -33,7 +33,7 @@ use nlp::bayes::{TokenHash, Weights}; use parking_lot::{Mutex, RwLock}; use rustls::sign::CertifiedKey; use std::{ - hash::{BuildHasher, Hasher}, + hash::{BuildHasher, Hash, Hasher}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, sync::{ Arc, @@ -46,7 +46,6 @@ use tinyvec::TinyVec; use tokio::sync::{Notify, Semaphore, mpsc}; use tokio_rustls::TlsConnector; use utils::{ - bimap::{IdBimap, IdBimapItem}, cache::{Cache, CacheItemWeight, CacheWithTtl}, snowflake::SnowflakeIdGenerator, }; @@ -149,7 +148,9 @@ pub struct Caches { pub permissions: Cache>, pub messages: Cache>, - pub dav: Cache>, + pub files: Cache>, + pub contacts: Cache>, + pub events: Cache>, pub bayes: CacheWithTtl, @@ -242,44 +243,72 @@ pub struct TlsConnectors { pub struct NameWrapper(pub String); -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub struct DavResourceId { - pub account_id: u32, - pub collection: u8, -} - -#[derive(Debug, Default)] +#[derive(Debug, Clone)] pub struct DavResources { pub base_path: String, - pub paths: IdBimap, + pub paths: AHashSet, + pub resources: Vec, + pub item_change_id: u64, + pub container_change_id: u64, + pub highest_change_id: u64, pub size: u64, - pub modseq: Option, + pub update_lock: Arc, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] +pub struct DavPath { + pub path: String, + pub parent_id: Option, + pub hierarchy_seq: u32, + pub resource_idx: usize, +} + +#[derive(Debug, Clone)] pub struct DavResource { pub document_id: u32, - pub parent_id: Option, - pub name: String, pub data: DavResourceMetadata, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone, Copy)] +pub struct DavResourcePath<'x> { + pub path: &'x DavPath, + pub resource: &'x DavResource, +} + +#[derive(Debug, Clone)] pub enum DavResourceMetadata { File { - size: u32, - hierarchy_sequence: u32, - is_container: bool, + name: String, + size: Option, + parent_id: Option, + acls: TinyVec<[AclGrant; 2]>, }, Calendar { + name: String, + acls: TinyVec<[AclGrant; 2]>, tz: Tz, }, CalendarEvent { + names: TinyVec<[DavName; 2]>, start: i64, duration: u32, }, - #[default] - None, + AddressBook { + name: String, + acls: TinyVec<[AclGrant; 2]>, + }, + ContactCard { + names: TinyVec<[DavName; 2]>, + }, +} + +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] +#[rkyv(derive(Debug))] +pub struct DavName { + pub name: String, + pub parent_id: u32, } #[derive(Clone, Default)] @@ -299,12 +328,6 @@ pub struct Core { pub enterprise: Option, } -impl CacheItemWeight for DavResourceId { - fn weight(&self) -> u64 { - std::mem::size_of::() as u64 - } -} - impl CacheItemWeight for CacheSwap { fn weight(&self) -> u64 { std::mem::size_of::>() as u64 + self.0.load().weight() @@ -428,7 +451,9 @@ impl Default for Caches { http_auth: Cache::new(1024, 10 * 1024 * 1024), permissions: Cache::new(1024, 10 * 1024 * 1024), messages: Cache::new(1024, 25 * 1024 * 1024), - dav: Cache::new(1024, 10 * 1024 * 1024), + files: Cache::new(1024, 10 * 1024 * 1024), + contacts: Cache::new(1024, 10 * 1024 * 1024), + events: Cache::new(1024, 10 * 1024 * 1024), bayes: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_rbl: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_txt: CacheWithTtl::new(1024, 10 * 1024 * 1024), @@ -480,45 +505,117 @@ pub fn ip_to_bytes_prefix(prefix: u8, ip: &IpAddr) -> Vec { } } +impl DavResourcePath<'_> { + #[inline(always)] + pub fn document_id(&self) -> u32 { + self.resource.document_id + } + + #[inline(always)] + pub fn parent_id(&self) -> Option { + self.path.parent_id + } + + #[inline(always)] + pub fn path(&self) -> &str { + self.path.path.as_str() + } + + #[inline(always)] + pub fn is_container(&self) -> bool { + self.resource.is_container() + } + + #[inline(always)] + pub fn hierarchy_seq(&self) -> u32 { + self.path.hierarchy_seq + } + + #[inline(always)] + pub fn size(&self) -> u32 { + self.resource.size() + } +} + impl DavResources { - pub fn subtree(&self, search_path: &str) -> impl Iterator { - let prefix = format!("{search_path}/"); - self.paths + pub fn by_path(&self, name: &str) -> Option> { + self.paths.get(name).map(|path| DavResourcePath { + path, + resource: &self.resources[path.resource_idx], + }) + } + + pub fn container_resource_by_id(&self, id: u32) -> Option<&DavResource> { + self.resources .iter() - .filter(move |item| item.name.starts_with(&prefix) || item.name == search_path) + .find(|res| res.document_id == id && res.is_container()) + } + + pub fn subtree(&self, search_path: &str) -> impl Iterator> { + let prefix = format!("{search_path}/"); + self.paths.iter().filter_map(move |path| { + if path.path.starts_with(&prefix) || path.path == search_path { + Some(DavResourcePath { + path, + resource: &self.resources[path.resource_idx], + }) + } else { + None + } + }) } pub fn subtree_with_depth( &self, search_path: &str, depth: usize, - ) -> impl Iterator { + ) -> impl Iterator> { let prefix = format!("{search_path}/"); - self.paths.iter().filter(move |item| { - item.name + self.paths.iter().filter_map(move |path| { + if path + .path .strip_prefix(&prefix) .is_some_and(|name| name.as_bytes().iter().filter(|&&c| c == b'/').count() < depth) - || item.name == search_path + || path.path.as_str() == search_path + { + Some(DavResourcePath { + path, + resource: &self.resources[path.resource_idx], + }) + } else { + None + } }) } - pub fn tree_with_depth(&self, depth: usize) -> impl Iterator { - self.paths.iter().filter(move |item| { - item.name.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth + pub fn tree_with_depth(&self, depth: usize) -> impl Iterator> { + self.paths.iter().filter_map(move |path| { + if path.path.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth { + Some(DavResourcePath { + path, + resource: &self.resources[path.resource_idx], + }) + } else { + None + } }) } - pub fn children(&self, parent_id: u32) -> impl Iterator { + pub fn children(&self, parent_id: u32) -> impl Iterator> { self.paths .iter() .filter(move |item| item.parent_id.is_some_and(|id| id == parent_id)) + .map(|path| DavResourcePath { + path, + resource: &self.resources[path.resource_idx], + }) } - pub fn format_resource(&self, resource: &DavResource) -> String { - if resource.is_container() { - format!("{}{}/", self.base_path, resource.name) + pub fn format_resource(&self, resource: DavResourcePath<'_>) -> String { + if resource.resource.is_container() { + format!("{}{}/", self.base_path, resource.path.path) } else { - format!("{}{}", self.base_path, resource.name) + format!("{}{}", self.base_path, resource.path.path) } } @@ -532,59 +629,128 @@ impl DavResources { } impl DavResource { + pub fn is_child_of(&self, parent_id: u32) -> bool { + match &self.data { + DavResourceMetadata::File { parent_id: id, .. } => id.is_some_and(|id| id == parent_id), + DavResourceMetadata::CalendarEvent { names, .. } => { + names.iter().any(|name| name.parent_id == parent_id) + } + DavResourceMetadata::ContactCard { names } => { + names.iter().any(|name| name.parent_id == parent_id) + } + _ => false, + } + } + + pub fn child_names(&self) -> Option<&[DavName]> { + match &self.data { + DavResourceMetadata::CalendarEvent { names, .. } => Some(names.as_slice()), + DavResourceMetadata::ContactCard { names } => Some(names.as_slice()), + _ => None, + } + } + + pub fn container_name(&self) -> Option<&str> { + match &self.data { + DavResourceMetadata::File { name, .. } => Some(name.as_str()), + DavResourceMetadata::Calendar { name, .. } => Some(name.as_str()), + DavResourceMetadata::AddressBook { name, .. } => Some(name.as_str()), + _ => None, + } + } + + pub fn has_hierarchy_changes(&self, other: &DavResource) -> bool { + match (&self.data, &other.data) { + ( + DavResourceMetadata::File { + name: a, + parent_id: c, + .. + }, + DavResourceMetadata::File { + name: b, + parent_id: d, + .. + }, + ) => a != b || c != d, + ( + DavResourceMetadata::Calendar { name: a, .. }, + DavResourceMetadata::Calendar { name: b, .. }, + ) => a != b, + ( + DavResourceMetadata::AddressBook { name: a, .. }, + DavResourceMetadata::AddressBook { name: b, .. }, + ) => a != b, + ( + DavResourceMetadata::CalendarEvent { names: a, .. }, + DavResourceMetadata::CalendarEvent { names: b, .. }, + ) => a != b, + ( + DavResourceMetadata::ContactCard { names: a, .. }, + DavResourceMetadata::ContactCard { names: b, .. }, + ) => a != b, + _ => unreachable!(), + } + } + pub fn event_time_range(&self) -> Option<(i64, i64)> { match &self.data { - DavResourceMetadata::CalendarEvent { start, duration } => { - Some((*start, *start + *duration as i64)) - } + DavResourceMetadata::CalendarEvent { + start, duration, .. + } => Some((*start, *start + *duration as i64)), _ => None, } } pub fn timezone(&self) -> Option { match &self.data { - DavResourceMetadata::Calendar { tz } => Some(*tz), + DavResourceMetadata::Calendar { tz, .. } => Some(*tz), _ => None, } } pub fn is_container(&self) -> bool { match &self.data { - DavResourceMetadata::File { is_container, .. } => *is_container, - _ => self.parent_id.is_none(), + DavResourceMetadata::File { size, .. } => size.is_none(), + DavResourceMetadata::Calendar { .. } | DavResourceMetadata::AddressBook { .. } => true, + _ => false, } } pub fn size(&self) -> u32 { match &self.data { - DavResourceMetadata::File { size, .. } => *size, + DavResourceMetadata::File { size, .. } => size.unwrap_or_default(), _ => 0, } } - pub fn hierarchy_sequence(&self) -> u32 { + pub fn acls(&self) -> Option<&[AclGrant]> { match &self.data { - DavResourceMetadata::File { - hierarchy_sequence, .. - } => *hierarchy_sequence, - _ => { - if self.parent_id.is_none() { - 0 - } else { - 1 - } - } + DavResourceMetadata::File { acls, .. } => Some(acls.as_slice()), + DavResourceMetadata::Calendar { acls, .. } => Some(acls.as_slice()), + DavResourceMetadata::AddressBook { acls, .. } => Some(acls.as_slice()), + _ => None, } } } -impl IdBimapItem for DavResource { - fn id(&self) -> &u32 { - &self.document_id +impl Hash for DavPath { + fn hash(&self, state: &mut H) { + self.path.hash(state); } +} - fn name(&self) -> &str { - &self.name +impl PartialEq for DavPath { + fn eq(&self, other: &Self) -> bool { + self.path == other.path + } +} + +impl Eq for DavPath {} + +impl std::borrow::Borrow for DavPath { + fn borrow(&self) -> &str { + &self.path } } @@ -608,6 +774,12 @@ impl std::borrow::Borrow for DavResource { } } +impl DavName { + pub fn new(name: String, parent_id: u32) -> Self { + Self { name, parent_id } + } +} + impl MessageStoreCache { pub fn assign_thread_id(&self, thread_name: &[u8], message_id: &[u8]) -> u32 { let mut bytes = Vec::with_capacity(thread_name.len() + message_id.len()); diff --git a/crates/common/src/sharing/mod.rs b/crates/common/src/sharing/mod.rs index e86f7c3e..8666d385 100644 --- a/crates/common/src/sharing/mod.rs +++ b/crates/common/src/sharing/mod.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::auth::AccessToken; use jmap_proto::types::{ acl::Acl, value::{AclGrant, ArchivedAclGrant}, @@ -11,10 +12,9 @@ use jmap_proto::types::{ use rkyv::vec::ArchivedVec; use utils::map::bitmap::Bitmap; -use crate::auth::AccessToken; - pub mod acl; pub mod document; +pub mod resources; pub trait EffectiveAcl { fn effective_acl(&self, access_token: &AccessToken) -> Bitmap; diff --git a/crates/common/src/sharing/resources.rs b/crates/common/src/sharing/resources.rs new file mode 100644 index 00000000..de487dad --- /dev/null +++ b/crates/common/src/sharing/resources.rs @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{DavResources, auth::AccessToken}; +use jmap_proto::types::acl::Acl; +use store::roaring::RoaringBitmap; +use utils::map::bitmap::Bitmap; + +impl DavResources { + pub fn shared_containers( + &self, + access_token: &AccessToken, + check_acls: impl IntoIterator, + match_any: bool, + ) -> RoaringBitmap { + let check_acls = Bitmap::::from_iter(check_acls); + let mut document_ids = RoaringBitmap::new(); + + for resource in &self.resources { + if let Some(acls) = resource.acls() { + for acl in acls { + if access_token.is_member(acl.account_id) { + let mut grants = acl.grants; + grants.intersection(&check_acls); + if grants == check_acls || (match_any && !grants.is_empty()) { + document_ids.insert(resource.document_id); + } + } + } + } + } + + document_ids + } + + pub fn has_access_to_container( + &self, + access_token: &AccessToken, + document_id: u32, + check_acls: impl Into>, + ) -> bool { + let check_acls = check_acls.into(); + + for resource in &self.resources { + if resource.document_id == document_id { + if let Some(acls) = resource.acls() { + for acl in acls { + if access_token.is_member(acl.account_id) { + let mut grants = acl.grants; + grants.intersection(&check_acls); + return !grants.is_empty(); + } + } + break; + } + } + } + + false + } + + pub fn container_acl(&self, access_token: &AccessToken, document_id: u32) -> Bitmap { + let mut account_acls = Bitmap::::new(); + + for resource in &self.resources { + if resource.document_id == document_id { + if let Some(acls) = resource.acls() { + for acl in acls { + if access_token.is_member(acl.account_id) { + account_acls.union(&acl.grants); + } + } + break; + } + } + } + + account_acls + } +} diff --git a/crates/common/src/storage/folder.rs b/crates/common/src/storage/folder.rs deleted file mode 100644 index 90e150ae..00000000 --- a/crates/common/src/storage/folder.rs +++ /dev/null @@ -1,217 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use ahash::AHashMap; -use jmap_proto::types::{collection::Collection, property::Property}; -use store::{ - Deserialize, IndexKey, IterateParams, SerializeInfallible, SerializedVersion, U32_LEN, - ValueKey, - write::{AlignedBytes, Archive, ValueClass, key::DeserializeBigEndian}, -}; -use trc::AddContext; -use utils::topological::{TopologicalSort, TopologicalSortIterator}; - -use crate::Server; - -pub struct ExpandedFolders { - names: AHashMap, - iter: TopologicalSortIterator, -} - -#[derive(Debug, Clone)] -pub struct ExpandedFolder { - pub name: String, - pub document_id: u32, - pub parent_id: Option, - pub is_container: bool, - pub size: u32, - pub hierarchy_sequence: u32, -} - -pub trait FolderHierarchy: Sync + Send { - fn name(&self) -> String; - fn parent_id(&self) -> u32; - fn is_container(&self) -> bool; - fn size(&self) -> u32; -} - -pub trait TopologyBuilder: Sync + Send { - fn insert(&mut self, folder_id: u32, parent_id: u32); -} - -impl Server { - pub async fn fetch_folders( - &self, - account_id: u32, - collection: Collection, - ) -> trc::Result - where - T: rkyv::Archive + SerializedVersion, - T::Archived: FolderHierarchy - + for<'a> rkyv::bytecheck::CheckBytes< - rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>, - > + rkyv::Deserialize>, - { - let collection_: u8 = collection.into(); - - let mut names = AHashMap::with_capacity(10); - let mut topological_sort = TopologicalSort::with_capacity(10); - - self.core - .storage - .data - .iterate( - IterateParams::new( - ValueKey { - account_id, - collection: collection_, - document_id: 0, - class: ValueClass::Property(Property::Value.into()), - }, - ValueKey { - account_id, - collection: collection_, - document_id: u32::MAX, - class: ValueClass::Property(Property::Value.into()), - }, - ), - |key, value| { - let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?; - let archive = as Deserialize>::deserialize(value)?; - let folder = archive.unarchive::()?; - let parent_id = folder.parent_id(); - - topological_sort.insert(parent_id, document_id + 1); - names.insert( - document_id, - ExpandedFolder { - name: folder.name(), - document_id, - parent_id: if parent_id > 0 { - Some(parent_id - 1) - } else { - None - }, - is_container: folder.is_container(), - size: folder.size(), - hierarchy_sequence: 0, - }, - ); - - Ok(true) - }, - ) - .await - .add_context(|err| { - err.caused_by(trc::location!()) - .account_id(account_id) - .collection(collection) - })?; - - Ok(ExpandedFolders { - names, - iter: topological_sort.into_iterator(), - }) - } - - pub async fn fetch_folder_topology( - &self, - account_id: u32, - collection: Collection, - topology: &mut impl TopologyBuilder, - ) -> trc::Result<()> - where - T: TopologyBuilder, - { - self.store() - .iterate( - IterateParams::new( - IndexKey { - account_id, - collection: collection.into(), - document_id: 0, - field: Property::ParentId.into(), - key: 0u32.serialize(), - }, - IndexKey { - account_id, - collection: collection.into(), - document_id: u32::MAX, - field: Property::ParentId.into(), - key: u32::MAX.serialize(), - }, - ) - .no_values() - .ascending(), - |key, _| { - let document_id = key - .get(key.len() - U32_LEN..) - .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!())) - .and_then(u32::deserialize)?; - let parent_id = key - .get(key.len() - (U32_LEN * 2)..key.len() - U32_LEN) - .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!())) - .and_then(u32::deserialize)?; - - topology.insert(document_id, parent_id); - - Ok(true) - }, - ) - .await - .caused_by(trc::location!())?; - - Ok(()) - } -} - -impl ExpandedFolders { - pub fn len(&self) -> usize { - self.names.len() - } - - pub fn is_empty(&self) -> bool { - self.names.is_empty() - } - - pub fn format(mut self, formatter: T) -> Self - where - T: Fn(&mut ExpandedFolder), - { - for folder in self.names.values_mut() { - formatter(folder); - } - self - } - - pub fn into_iterator(mut self) -> impl Iterator + Sync + Send { - for (hierarchy_sequence, folder_id) in self.iter.by_ref().enumerate() { - if folder_id != 0 { - let folder_id = folder_id - 1; - if let Some((name, parent_name)) = self - .names - .get(&folder_id) - .and_then(|folder| folder.parent_id.map(|parent_id| (&folder.name, parent_id))) - .and_then(|(name, parent_id)| { - self.names - .get(&parent_id) - .map(|folder| (name, &folder.name)) - }) - { - let name = format!("{parent_name}/{name}"); - let folder = self.names.get_mut(&folder_id).unwrap(); - folder.name = name; - folder.hierarchy_sequence = hierarchy_sequence as u32; - } else { - self.names.get_mut(&folder_id).unwrap().hierarchy_sequence = - hierarchy_sequence as u32; - } - } - } - - self.names.into_values() - } -} diff --git a/crates/common/src/storage/mod.rs b/crates/common/src/storage/mod.rs index e7f4eec2..f31f6002 100644 --- a/crates/common/src/storage/mod.rs +++ b/crates/common/src/storage/mod.rs @@ -5,6 +5,5 @@ */ pub mod blob; -pub mod folder; pub mod index; pub mod state; diff --git a/crates/dav/src/calendar/copy_move.rs b/crates/dav/src/calendar/copy_move.rs index 9e9fe57f..eb729992 100644 --- a/crates/dav/src/calendar/copy_move.rs +++ b/crates/dav/src/calendar/copy_move.rs @@ -4,16 +4,19 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; +use common::{DavName, Server, auth::AccessToken}; use dav_proto::{Depth, RequestHeaders}; use groupware::{ - DavName, DestroyArchive, + DestroyArchive, + cache::GroupwareCache, calendar::{Calendar, CalendarEvent, CalendarPreferences, Timezone}, - hierarchy::DavHierarchy, }; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use store::write::BatchBuilder; use trc::AddContext; @@ -51,33 +54,27 @@ impl CalendarCopyMoveRequestHandler for Server { .into_owned_uri()?; let from_account_id = from_resource_.account_id; let from_resources = self - .fetch_dav_resources(access_token, from_account_id, Collection::Calendar) + .fetch_dav_resources(access_token, from_account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())?; let from_resource_name = from_resource_ .resource .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; let from_resource = from_resources - .paths - .by_name(from_resource_name) + .by_path(from_resource_name) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; // Validate ACL if !access_token.is_member(from_account_id) - && !self - .has_access_to_document( - access_token, - from_account_id, - Collection::Calendar, - if from_resource.is_container() { - from_resource.document_id - } else { - from_resource.parent_id.unwrap() - }, - Acl::ReadItems, - ) - .await - .caused_by(trc::location!())? + && !from_resources.has_access_to_container( + access_token, + if from_resource.is_container() { + from_resource.document_id() + } else { + from_resource.parent_id().unwrap() + }, + Acl::ReadItems, + ) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -101,7 +98,7 @@ impl CalendarCopyMoveRequestHandler for Server { let to_resources = if to_account_id == from_account_id { from_resources.clone() } else { - self.fetch_dav_resources(access_token, to_account_id, Collection::Calendar) + self.fetch_dav_resources(access_token, to_account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())? }; @@ -110,7 +107,7 @@ impl CalendarCopyMoveRequestHandler for Server { let destination_resource_name = destination .resource .ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?; - let to_resource = to_resources.paths.by_name(destination_resource_name); + let to_resource = to_resources.by_path(destination_resource_name); self.validate_headers( access_token, &headers, @@ -122,7 +119,7 @@ impl CalendarCopyMoveRequestHandler for Server { } else { Collection::CalendarEvent }, - document_id: Some(from_resource.document_id), + document_id: Some(from_resource.document_id()), path: from_resource_name, ..Default::default() }, @@ -137,7 +134,7 @@ impl CalendarCopyMoveRequestHandler for Server { } }) .unwrap_or(Collection::Calendar), - document_id: Some(to_resource.map(|r| r.document_id).unwrap_or(u32::MAX)), + document_id: Some(to_resource.map(|r| r.document_id()).unwrap_or(u32::MAX)), path: destination_resource_name, ..Default::default() }, @@ -153,7 +150,7 @@ impl CalendarCopyMoveRequestHandler for Server { // Map destination if let Some(to_resource) = to_resource { - if from_resource.name == to_resource.name { + if from_resource.path() == to_resource.path() { // Same resource return Err(DavError::Code(StatusCode::BAD_GATEWAY)); } @@ -167,31 +164,26 @@ impl CalendarCopyMoveRequestHandler for Server { let from_children_ids = from_resources .subtree(from_resource_name) .filter(|r| !r.is_container()) - .map(|r| r.document_id) + .map(|r| r.document_id()) .collect::>(); let to_document_ids = to_resources .subtree(destination_resource_name) .filter(|r| !r.is_container()) - .map(|r| r.document_id) + .map(|r| r.document_id()) .collect::>(); // Validate ACLs if !access_token.is_member(to_account_id) || (!access_token.is_member(from_account_id) - && !self - .has_access_to_document( - access_token, - from_account_id, - Collection::Calendar, - from_resource.document_id, - if is_move { - Acl::RemoveItems - } else { - Acl::ReadItems - }, - ) - .await - .caused_by(trc::location!())?) + && !from_resources.has_access_to_container( + access_token, + from_resource.document_id(), + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + )) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -201,10 +193,10 @@ impl CalendarCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), from_children_ids, to_account_id, - to_resource.document_id.into(), + to_resource.document_id().into(), to_document_ids, new_name, is_move, @@ -213,36 +205,26 @@ impl CalendarCopyMoveRequestHandler for Server { } (false, false) => { // Overwrite event - let from_calendar_id = from_resource.parent_id.unwrap(); - let to_calendar_id = to_resource.parent_id.unwrap(); + let from_calendar_id = from_resource.parent_id().unwrap(); + let to_calendar_id = to_resource.parent_id().unwrap(); // Validate ACL if (!access_token.is_member(from_account_id) - && !self - .has_access_to_document( - access_token, - from_account_id, - Collection::Calendar, - from_calendar_id, - if is_move { - Acl::RemoveItems - } else { - Acl::ReadItems - }, - ) - .await - .caused_by(trc::location!())?) + && !from_resources.has_access_to_container( + access_token, + from_calendar_id, + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + )) || (!access_token.is_member(to_account_id) - && !self - .has_access_to_document( - access_token, - to_account_id, - Collection::Calendar, - to_calendar_id, - Acl::RemoveItems, - ) - .await - .caused_by(trc::location!())?) + && !to_resources.has_access_to_container( + access_token, + to_calendar_id, + Acl::RemoveItems, + )) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -252,10 +234,10 @@ impl CalendarCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), from_calendar_id, to_account_id, - to_resource.document_id.into(), + to_resource.document_id().into(), to_calendar_id, new_name, ) @@ -265,9 +247,9 @@ impl CalendarCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), to_account_id, - to_resource.document_id.into(), + to_resource.document_id().into(), to_calendar_id, new_name, ) @@ -287,34 +269,24 @@ impl CalendarCopyMoveRequestHandler for Server { } // Validate ACL - let from_calendar_id = from_resource.parent_id.unwrap(); - let to_calendar_id = parent_resource.document_id; + let from_calendar_id = from_resource.parent_id().unwrap(); + let to_calendar_id = parent_resource.document_id(); if (!access_token.is_member(from_account_id) - && !self - .has_access_to_document( - access_token, - from_account_id, - Collection::Calendar, - from_calendar_id, - if is_move { - Acl::RemoveItems - } else { - Acl::ReadItems - }, - ) - .await - .caused_by(trc::location!())?) + && !from_resources.has_access_to_container( + access_token, + from_calendar_id, + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + )) || (!access_token.is_member(to_account_id) - && !self - .has_access_to_document( - access_token, - to_account_id, - Collection::Calendar, - to_calendar_id, - Acl::AddItems, - ) - .await - .caused_by(trc::location!())?) + && !to_resources.has_access_to_container( + access_token, + to_calendar_id, + Acl::AddItems, + )) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -322,13 +294,13 @@ impl CalendarCopyMoveRequestHandler for Server { // Copy/move event if is_move { if from_account_id != to_account_id - || parent_resource.document_id != from_calendar_id + || parent_resource.document_id() != from_calendar_id { move_event( self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), from_calendar_id, to_account_id, None, @@ -341,7 +313,7 @@ impl CalendarCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), from_calendar_id, new_name, ) @@ -352,7 +324,7 @@ impl CalendarCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), to_account_id, None, to_calendar_id, @@ -373,20 +345,15 @@ impl CalendarCopyMoveRequestHandler for Server { // Validate ACLs if !access_token.is_member(from_account_id) - && !self - .has_access_to_document( - access_token, - from_account_id, - Collection::Calendar, - from_resource.document_id, - if is_move { - Acl::RemoveItems - } else { - Acl::ReadItems - }, - ) - .await - .caused_by(trc::location!())? + && !from_resources.has_access_to_container( + access_token, + from_resource.document_id(), + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + ) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -395,7 +362,7 @@ impl CalendarCopyMoveRequestHandler for Server { let from_children_ids = from_resources .subtree(from_resource_name) .filter(|r| !r.is_container()) - .map(|r| r.document_id) + .map(|r| r.document_id()) .collect::>(); if is_move { if from_account_id != to_account_id { @@ -403,7 +370,7 @@ impl CalendarCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), if headers.depth != Depth::Zero { from_children_ids } else { @@ -421,7 +388,7 @@ impl CalendarCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), new_name, ) .await @@ -431,7 +398,7 @@ impl CalendarCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), if headers.depth != Depth::Zero { from_children_ids } else { @@ -478,7 +445,7 @@ async fn copy_event( assert_is_unique_uid( server, server - .fetch_dav_resources(access_token, to_account_id, Collection::Calendar) + .fetch_dav_resources(access_token, to_account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())? .as_ref(), @@ -588,7 +555,7 @@ async fn move_event( assert_is_unique_uid( server, server - .fetch_dav_resources(access_token, to_account_id, Collection::Calendar) + .fetch_dav_resources(access_token, to_account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())? .as_ref(), diff --git a/crates/dav/src/calendar/delete.rs b/crates/dav/src/calendar/delete.rs index 52edf1a5..cfac27b7 100644 --- a/crates/dav/src/calendar/delete.rs +++ b/crates/dav/src/calendar/delete.rs @@ -8,12 +8,15 @@ use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::RequestHeaders; use groupware::{ DestroyArchive, + cache::GroupwareCache, calendar::{Calendar, CalendarEvent}, - hierarchy::DavHierarchy, }; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use store::write::BatchBuilder; use trc::AddContext; @@ -51,16 +54,15 @@ impl CalendarDeleteRequestHandler for Server { .filter(|r| !r.is_empty()) .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; let resources = self - .fetch_dav_resources(access_token, account_id, Collection::Calendar) + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())?; // Check resource type let delete_resource = resources - .paths - .by_name(delete_path) + .by_path(delete_path) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - let document_id = delete_resource.document_id; + let document_id = delete_resource.document_id(); // Fetch entry let mut batch = BatchBuilder::new(); @@ -113,7 +115,7 @@ impl CalendarDeleteRequestHandler for Server { resources .subtree(delete_path) .filter(|r| !r.is_container()) - .map(|r| r.document_id) + .map(|r| r.document_id()) .collect::>(), &mut batch, ) @@ -121,18 +123,9 @@ impl CalendarDeleteRequestHandler for Server { .caused_by(trc::location!())?; } else { // Validate ACL - let addresscalendar_id = delete_resource.parent_id.unwrap(); + let calendar_id = delete_resource.parent_id().unwrap(); if !access_token.is_member(account_id) - && !self - .has_access_to_document( - access_token, - account_id, - Collection::Calendar, - addresscalendar_id, - Acl::RemoveItems, - ) - .await - .caused_by(trc::location!())? + && !resources.has_access_to_container(access_token, calendar_id, Acl::RemoveItems) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -170,7 +163,7 @@ impl CalendarDeleteRequestHandler for Server { access_token, account_id, document_id, - addresscalendar_id, + calendar_id, &mut batch, ) .caused_by(trc::location!())?; diff --git a/crates/dav/src/calendar/freebusy.rs b/crates/dav/src/calendar/freebusy.rs index dec28eea..38db6403 100644 --- a/crates/dav/src/calendar/freebusy.rs +++ b/crates/dav/src/calendar/freebusy.rs @@ -23,10 +23,13 @@ use dav_proto::{ RequestHeaders, schema::{property::TimeRange, request::FreeBusyQuery}, }; -use groupware::{calendar::CalendarEvent, hierarchy::DavHierarchy}; +use groupware::{cache::GroupwareCache, calendar::CalendarEvent}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use store::{ ahash::AHashMap, write::{now, serialize::rkyv_deserialize}, @@ -56,12 +59,11 @@ impl CalendarFreebusyRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(access_token, account_id, Collection::Calendar) + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())?; let resource = resources - .paths - .by_name( + .by_path( resource_ .resource .ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?, @@ -70,20 +72,13 @@ impl CalendarFreebusyRequestHandler for Server { if !resource.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } - let default_tz = resource.timezone().unwrap_or(Tz::UTC); + let default_tz = resource.resource.timezone().unwrap_or(Tz::UTC); // Obtain shared ids let shared_ids = if !access_token.is_member(account_id) { - self.shared_containers( - access_token, - account_id, - Collection::Calendar, - [Acl::ReadItems, Acl::ReadFreeBusy], - false, - ) - .await - .caused_by(trc::location!())? - .into() + resources + .shared_containers(access_token, [Acl::ReadItems, Acl::ReadFreeBusy], false) + .into() } else { None }; @@ -114,14 +109,14 @@ impl CalendarFreebusyRequestHandler for Server { }); let document_ids = resources - .children(resource.document_id) + .children(resource.document_id()) .filter(|resource| { shared_ids .as_ref() - .is_none_or(|ids| ids.contains(resource.document_id)) - && is_resource_in_time_range(resource, &range) + .is_none_or(|ids| ids.contains(resource.document_id())) + && is_resource_in_time_range(resource.resource, &range) }) - .map(|resource| resource.document_id) + .map(|resource| resource.document_id()) .collect::>(); let mut fb_entries: AHashMap> = diff --git a/crates/dav/src/calendar/get.rs b/crates/dav/src/calendar/get.rs index b07d9d77..2c981c64 100644 --- a/crates/dav/src/calendar/get.rs +++ b/crates/dav/src/calendar/get.rs @@ -6,10 +6,13 @@ use common::{Server, auth::AccessToken}; use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime}; -use groupware::{calendar::CalendarEvent, hierarchy::DavHierarchy}; +use groupware::{cache::GroupwareCache, calendar::CalendarEvent}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use trc::AddContext; use crate::{ @@ -44,12 +47,11 @@ impl CalendarGetRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(access_token, account_id, Collection::Calendar) + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())?; let resource = resources - .paths - .by_name( + .by_path( resource_ .resource .ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?, @@ -61,23 +63,22 @@ impl CalendarGetRequestHandler for Server { // Validate ACL if !access_token.is_member(account_id) - && !self - .has_access_to_document( - access_token, - account_id, - Collection::Calendar, - resource.parent_id.unwrap(), - Acl::ReadItems, - ) - .await - .caused_by(trc::location!())? + && !resources.has_access_to_container( + access_token, + resource.parent_id().unwrap(), + Acl::ReadItems, + ) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } // Fetch event let event_ = self - .get_archive(account_id, Collection::CalendarEvent, resource.document_id) + .get_archive( + account_id, + Collection::CalendarEvent, + resource.document_id(), + ) .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; @@ -93,7 +94,7 @@ impl CalendarGetRequestHandler for Server { vec![ResourceState { account_id, collection: Collection::CalendarEvent, - document_id: resource.document_id.into(), + document_id: resource.document_id().into(), etag: etag.clone().into(), path: resource_.resource.unwrap(), ..Default::default() diff --git a/crates/dav/src/calendar/mkcol.rs b/crates/dav/src/calendar/mkcol.rs index 738faffc..dc762125 100644 --- a/crates/dav/src/calendar/mkcol.rs +++ b/crates/dav/src/calendar/mkcol.rs @@ -9,13 +9,10 @@ use dav_proto::{ RequestHeaders, Return, schema::{Namespace, request::MkCol, response::MkColResponse}, }; -use groupware::{ - calendar::{Calendar, CalendarPreferences}, - hierarchy::DavHierarchy, -}; +use groupware::{cache::GroupwareCache, calendar::{Calendar, CalendarPreferences}}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::collection::Collection; +use jmap_proto::types::collection::{Collection, SyncCollection}; use store::write::BatchBuilder; use trc::AddContext; @@ -59,11 +56,11 @@ impl CalendarMkColRequestHandler for Server { return Err(DavError::Code(StatusCode::FORBIDDEN)); } else if name.contains('/') || self - .fetch_dav_resources(access_token, account_id, Collection::Calendar) + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())? - .paths - .by_name(name) + + .by_path(name) .is_some() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); diff --git a/crates/dav/src/calendar/mod.rs b/crates/dav/src/calendar/mod.rs index 944002b4..c72d3adf 100644 --- a/crates/dav/src/calendar/mod.rs +++ b/crates/dav/src/calendar/mod.rs @@ -106,11 +106,8 @@ pub(crate) async fn assert_is_unique_uid( .caused_by(trc::location!())?; if !hits.results.is_empty() { - for path in resources.paths.iter() { - if !path.is_container() - && hits.results.contains(path.document_id) - && path.parent_id.unwrap() == calendar_id - { + for path in resources.children(calendar_id) { + if hits.results.contains(path.document_id()) { return Err(DavError::Condition(DavErrorCondition::new( StatusCode::PRECONDITION_FAILED, CalCondition::NoUidConflict(resources.format_resource(path).into()), diff --git a/crates/dav/src/calendar/proppatch.rs b/crates/dav/src/calendar/proppatch.rs index f0da8ce3..c53fd2f9 100644 --- a/crates/dav/src/calendar/proppatch.rs +++ b/crates/dav/src/calendar/proppatch.rs @@ -26,12 +26,15 @@ use dav_proto::{ }, }; use groupware::{ + cache::GroupwareCache, calendar::{Calendar, CalendarEvent, Timezone}, - hierarchy::DavHierarchy, }; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use store::write::BatchBuilder; use trc::AddContext; @@ -76,14 +79,14 @@ impl CalendarPropPatchRequestHandler for Server { let uri = headers.uri; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(access_token, account_id, Collection::Calendar) + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())?; let resource = resource_ .resource - .and_then(|r| resources.paths.by_name(r)) + .and_then(|r| resources.by_path(r)) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - let document_id = resource.document_id; + let document_id = resource.document_id(); let collection = if resource.is_container() { Collection::Calendar } else { @@ -97,22 +100,12 @@ impl CalendarPropPatchRequestHandler for Server { // Verify ACL if !access_token.is_member(account_id) { let (acl, document_id) = if resource.is_container() { - (Acl::Modify, resource.document_id) + (Acl::Modify, resource.document_id()) } else { - (Acl::ModifyItems, resource.parent_id.unwrap()) + (Acl::ModifyItems, resource.parent_id().unwrap()) }; - if !self - .has_access_to_document( - access_token, - account_id, - Collection::Calendar, - document_id, - acl, - ) - .await - .caused_by(trc::location!())? - { + if !resources.has_access_to_container(access_token, document_id, acl) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } } diff --git a/crates/dav/src/calendar/query.rs b/crates/dav/src/calendar/query.rs index db3fd32d..07afa87c 100644 --- a/crates/dav/src/calendar/query.rs +++ b/crates/dav/src/calendar/query.rs @@ -29,10 +29,10 @@ use dav_proto::{ request::{CalendarQuery, Filter, FilterOp, PropFind, Timezone}, }, }; -use groupware::{calendar::ArchivedCalendarEvent, hierarchy::DavHierarchy}; +use groupware::{cache::GroupwareCache, calendar::ArchivedCalendarEvent}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{acl::Acl, collection::SyncCollection}; use std::{fmt::Write, slice::Iter, str::FromStr}; use store::{ahash::AHashMap, write::serialize::rkyv_deserialize}; use trc::AddContext; @@ -62,12 +62,11 @@ impl CalendarQueryRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(access_token, account_id, Collection::Calendar) + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())?; let resource = resources - .paths - .by_name( + .by_path( resource_ .resource .ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?, @@ -79,16 +78,9 @@ impl CalendarQueryRequestHandler for Server { // Obtain shared ids let shared_ids = if !access_token.is_member(account_id) { - self.shared_containers( - access_token, - account_id, - Collection::Calendar, - [Acl::ReadItems], - false, - ) - .await - .caused_by(trc::location!())? - .into() + resources + .shared_containers(access_token, [Acl::ReadItems], false) + .into() } else { None }; @@ -98,13 +90,13 @@ impl CalendarQueryRequestHandler for Server { // Obtain document ids in folder let mut items = Vec::with_capacity(16); - for resource in resources.children(resource.document_id) { + for resource in resources.children(resource.document_id()) { if shared_ids .as_ref() - .is_none_or(|ids| ids.contains(resource.document_id)) + .is_none_or(|ids| ids.contains(resource.document_id())) && filter_range .as_ref() - .is_none_or(|range| is_resource_in_time_range(resource, range)) + .is_none_or(|range| is_resource_in_time_range(resource.resource, range)) { items.push(PropFindItem::new( resources.format_resource(resource), @@ -134,7 +126,7 @@ pub(crate) fn is_resource_in_time_range(resource: &DavResource, filter: &TimeRan let c = println!( "filter from {range_from} to {range_end}, resource is {} from {} to {}, result: {}", - resource.name, + resource.path(), DateTime::from_timestamp(start, 0).unwrap(), DateTime::from_timestamp(end, 0).unwrap(), result diff --git a/crates/dav/src/calendar/update.rs b/crates/dav/src/calendar/update.rs index ad39a85b..56def266 100644 --- a/crates/dav/src/calendar/update.rs +++ b/crates/dav/src/calendar/update.rs @@ -11,19 +11,21 @@ use calcard::{ common::timezone::Tz, icalendar::{ICalendar, ICalendarComponentType}, }; -use common::{Server, auth::AccessToken}; +use common::{DavName, Server, auth::AccessToken}; use dav_proto::{ RequestHeaders, Return, schema::{property::Rfc1123DateTime, response::CalCondition}, }; use groupware::{ - DavName, + cache::GroupwareCache, calendar::{CalendarEvent, CalendarEventData}, - hierarchy::DavHierarchy, }; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use store::write::BatchBuilder; use trc::AddContext; @@ -64,7 +66,7 @@ impl CalendarUpdateRequestHandler for Server { .into_owned_uri()?; let account_id = resource.account_id; let resources = self - .fetch_dav_resources(access_token, account_id, Collection::Calendar) + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await .caused_by(trc::location!())?; let resource_name = resource @@ -94,25 +96,16 @@ impl CalendarUpdateRequestHandler for Server { } }; - if let Some(resource) = resources.paths.by_name(resource_name) { + if let Some(resource) = resources.by_path(resource_name) { if resource.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } // Validate ACL - let parent_id = resource.parent_id.unwrap(); - let document_id = resource.document_id; + let parent_id = resource.parent_id().unwrap(); + let document_id = resource.document_id(); if !access_token.is_member(account_id) - && !self - .has_access_to_document( - access_token, - account_id, - Collection::Calendar, - parent_id, - Acl::ModifyItems, - ) - .await - .caused_by(trc::location!())? + && !resources.has_access_to_container(access_token, parent_id, Acl::ModifyItems) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -204,16 +197,11 @@ impl CalendarUpdateRequestHandler for Server { // Validate ACL if !access_token.is_member(account_id) - && !self - .has_access_to_document( - access_token, - account_id, - Collection::Calendar, - parent.document_id, - Acl::AddItems, - ) - .await - .caused_by(trc::location!())? + && !resources.has_access_to_container( + access_token, + parent.document_id(), + Acl::AddItems, + ) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -248,7 +236,7 @@ impl CalendarUpdateRequestHandler for Server { self, &resources, account_id, - parent.document_id, + parent.document_id(), validate_ical(&ical)?.into(), ) .await?; @@ -257,7 +245,7 @@ impl CalendarUpdateRequestHandler for Server { let event = CalendarEvent { names: vec![DavName { name: name.to_string(), - parent_id: parent.document_id, + parent_id: parent.document_id(), }], data: CalendarEventData::new( ical, diff --git a/crates/dav/src/card/copy_move.rs b/crates/dav/src/card/copy_move.rs index 6a6f7eb3..80e484ac 100644 --- a/crates/dav/src/card/copy_move.rs +++ b/crates/dav/src/card/copy_move.rs @@ -4,16 +4,19 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; +use common::{DavName, Server, auth::AccessToken}; use dav_proto::{Depth, RequestHeaders}; use groupware::{ - DavName, DestroyArchive, + DestroyArchive, + cache::GroupwareCache, contact::{AddressBook, ContactCard}, - hierarchy::DavHierarchy, }; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use store::write::BatchBuilder; use trc::AddContext; @@ -51,33 +54,27 @@ impl CardCopyMoveRequestHandler for Server { .into_owned_uri()?; let from_account_id = from_resource_.account_id; let from_resources = self - .fetch_dav_resources(access_token, from_account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, from_account_id, SyncCollection::AddressBook) .await .caused_by(trc::location!())?; let from_resource_name = from_resource_ .resource .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; let from_resource = from_resources - .paths - .by_name(from_resource_name) + .by_path(from_resource_name) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; // Validate ACL if !access_token.is_member(from_account_id) - && !self - .has_access_to_document( - access_token, - from_account_id, - Collection::AddressBook, - if from_resource.is_container() { - from_resource.document_id - } else { - from_resource.parent_id.unwrap() - }, - Acl::ReadItems, - ) - .await - .caused_by(trc::location!())? + && !from_resources.has_access_to_container( + access_token, + if from_resource.is_container() { + from_resource.document_id() + } else { + from_resource.parent_id().unwrap() + }, + Acl::ReadItems, + ) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -101,7 +98,7 @@ impl CardCopyMoveRequestHandler for Server { let to_resources = if to_account_id == from_account_id { from_resources.clone() } else { - self.fetch_dav_resources(access_token, to_account_id, Collection::AddressBook) + self.fetch_dav_resources(access_token, to_account_id, SyncCollection::AddressBook) .await .caused_by(trc::location!())? }; @@ -110,7 +107,7 @@ impl CardCopyMoveRequestHandler for Server { let destination_resource_name = destination .resource .ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?; - let to_resource = to_resources.paths.by_name(destination_resource_name); + let to_resource = to_resources.by_path(destination_resource_name); self.validate_headers( access_token, &headers, @@ -122,7 +119,7 @@ impl CardCopyMoveRequestHandler for Server { } else { Collection::ContactCard }, - document_id: Some(from_resource.document_id), + document_id: Some(from_resource.document_id()), path: from_resource_name, ..Default::default() }, @@ -137,7 +134,7 @@ impl CardCopyMoveRequestHandler for Server { } }) .unwrap_or(Collection::AddressBook), - document_id: Some(to_resource.map(|r| r.document_id).unwrap_or(u32::MAX)), + document_id: Some(to_resource.map(|r| r.document_id()).unwrap_or(u32::MAX)), path: destination_resource_name, ..Default::default() }, @@ -153,7 +150,7 @@ impl CardCopyMoveRequestHandler for Server { // Map destination if let Some(to_resource) = to_resource { - if from_resource.name == to_resource.name { + if from_resource.path() == to_resource.path() { // Same resource return Err(DavError::Code(StatusCode::BAD_GATEWAY)); } @@ -167,31 +164,26 @@ impl CardCopyMoveRequestHandler for Server { let from_children_ids = from_resources .subtree(from_resource_name) .filter(|r| !r.is_container()) - .map(|r| r.document_id) + .map(|r| r.document_id()) .collect::>(); let to_document_ids = to_resources .subtree(destination_resource_name) .filter(|r| !r.is_container()) - .map(|r| r.document_id) + .map(|r| r.document_id()) .collect::>(); // Validate ACLs if !access_token.is_member(to_account_id) || (!access_token.is_member(from_account_id) - && !self - .has_access_to_document( - access_token, - from_account_id, - Collection::AddressBook, - from_resource.document_id, - if is_move { - Acl::RemoveItems - } else { - Acl::ReadItems - }, - ) - .await - .caused_by(trc::location!())?) + && !from_resources.has_access_to_container( + access_token, + from_resource.document_id(), + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + )) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -201,10 +193,10 @@ impl CardCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), from_children_ids, to_account_id, - to_resource.document_id.into(), + to_resource.document_id().into(), to_document_ids, new_name, is_move, @@ -213,36 +205,26 @@ impl CardCopyMoveRequestHandler for Server { } (false, false) => { // Overwrite card - let from_addressbook_id = from_resource.parent_id.unwrap(); - let to_addressbook_id = to_resource.parent_id.unwrap(); + let from_addressbook_id = from_resource.parent_id().unwrap(); + let to_addressbook_id = to_resource.parent_id().unwrap(); // Validate ACL if (!access_token.is_member(from_account_id) - && !self - .has_access_to_document( - access_token, - from_account_id, - Collection::AddressBook, - from_addressbook_id, - if is_move { - Acl::RemoveItems - } else { - Acl::ReadItems - }, - ) - .await - .caused_by(trc::location!())?) + && !from_resources.has_access_to_container( + access_token, + from_addressbook_id, + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + )) || (!access_token.is_member(to_account_id) - && !self - .has_access_to_document( - access_token, - to_account_id, - Collection::AddressBook, - to_addressbook_id, - Acl::RemoveItems, - ) - .await - .caused_by(trc::location!())?) + && !to_resources.has_access_to_container( + access_token, + to_addressbook_id, + Acl::RemoveItems, + )) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -252,10 +234,10 @@ impl CardCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), from_addressbook_id, to_account_id, - to_resource.document_id.into(), + to_resource.document_id().into(), to_addressbook_id, new_name, ) @@ -265,9 +247,9 @@ impl CardCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), to_account_id, - to_resource.document_id.into(), + to_resource.document_id().into(), to_addressbook_id, new_name, ) @@ -287,34 +269,24 @@ impl CardCopyMoveRequestHandler for Server { } // Validate ACL - let from_addressbook_id = from_resource.parent_id.unwrap(); - let to_addressbook_id = parent_resource.document_id; + let from_addressbook_id = from_resource.parent_id().unwrap(); + let to_addressbook_id = parent_resource.document_id(); if (!access_token.is_member(from_account_id) - && !self - .has_access_to_document( - access_token, - from_account_id, - Collection::AddressBook, - from_addressbook_id, - if is_move { - Acl::RemoveItems - } else { - Acl::ReadItems - }, - ) - .await - .caused_by(trc::location!())?) + && !from_resources.has_access_to_container( + access_token, + from_addressbook_id, + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + )) || (!access_token.is_member(to_account_id) - && !self - .has_access_to_document( - access_token, - to_account_id, - Collection::AddressBook, - to_addressbook_id, - Acl::AddItems, - ) - .await - .caused_by(trc::location!())?) + && !to_resources.has_access_to_container( + access_token, + to_addressbook_id, + Acl::AddItems, + )) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -322,13 +294,13 @@ impl CardCopyMoveRequestHandler for Server { // Copy/move card if is_move { if from_account_id != to_account_id - || parent_resource.document_id != from_addressbook_id + || parent_resource.document_id() != from_addressbook_id { move_card( self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), from_addressbook_id, to_account_id, None, @@ -341,7 +313,7 @@ impl CardCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), from_addressbook_id, new_name, ) @@ -352,7 +324,7 @@ impl CardCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), to_account_id, None, to_addressbook_id, @@ -373,20 +345,15 @@ impl CardCopyMoveRequestHandler for Server { // Validate ACLs if !access_token.is_member(from_account_id) - && !self - .has_access_to_document( - access_token, - from_account_id, - Collection::AddressBook, - from_resource.document_id, - if is_move { - Acl::RemoveItems - } else { - Acl::ReadItems - }, - ) - .await - .caused_by(trc::location!())? + && !from_resources.has_access_to_container( + access_token, + from_resource.document_id(), + if is_move { + Acl::RemoveItems + } else { + Acl::ReadItems + }, + ) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -395,7 +362,7 @@ impl CardCopyMoveRequestHandler for Server { let from_children_ids = from_resources .subtree(from_resource_name) .filter(|r| !r.is_container()) - .map(|r| r.document_id) + .map(|r| r.document_id()) .collect::>(); if is_move { if from_account_id != to_account_id { @@ -403,7 +370,7 @@ impl CardCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), if headers.depth != Depth::Zero { from_children_ids } else { @@ -421,7 +388,7 @@ impl CardCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), new_name, ) .await @@ -431,7 +398,7 @@ impl CardCopyMoveRequestHandler for Server { self, access_token, from_account_id, - from_resource.document_id, + from_resource.document_id(), if headers.depth != Depth::Zero { from_children_ids } else { @@ -478,7 +445,7 @@ async fn copy_card( assert_is_unique_uid( server, server - .fetch_dav_resources(access_token, to_account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, to_account_id, SyncCollection::AddressBook) .await .caused_by(trc::location!())? .as_ref(), @@ -588,7 +555,7 @@ async fn move_card( assert_is_unique_uid( server, server - .fetch_dav_resources(access_token, to_account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, to_account_id, SyncCollection::AddressBook) .await .caused_by(trc::location!())? .as_ref(), diff --git a/crates/dav/src/card/delete.rs b/crates/dav/src/card/delete.rs index 61b87fcd..124bef74 100644 --- a/crates/dav/src/card/delete.rs +++ b/crates/dav/src/card/delete.rs @@ -8,12 +8,15 @@ use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::RequestHeaders; use groupware::{ DestroyArchive, + cache::GroupwareCache, contact::{AddressBook, ContactCard}, - hierarchy::DavHierarchy, }; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use store::write::BatchBuilder; use trc::AddContext; @@ -51,16 +54,15 @@ impl CardDeleteRequestHandler for Server { .filter(|r| !r.is_empty()) .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; let resources = self - .fetch_dav_resources(access_token, account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, SyncCollection::AddressBook) .await .caused_by(trc::location!())?; // Check resource type let delete_resource = resources - .paths - .by_name(delete_path) + .by_path(delete_path) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - let document_id = delete_resource.document_id; + let document_id = delete_resource.document_id(); // Fetch entry let mut batch = BatchBuilder::new(); @@ -113,7 +115,7 @@ impl CardDeleteRequestHandler for Server { resources .subtree(delete_path) .filter(|r| !r.is_container()) - .map(|r| r.document_id) + .map(|r| r.document_id()) .collect::>(), &mut batch, ) @@ -121,18 +123,13 @@ impl CardDeleteRequestHandler for Server { .caused_by(trc::location!())?; } else { // Validate ACL - let addressbook_id = delete_resource.parent_id.unwrap(); + let addressbook_id = delete_resource.parent_id().unwrap(); if !access_token.is_member(account_id) - && !self - .has_access_to_document( - access_token, - account_id, - Collection::AddressBook, - addressbook_id, - Acl::RemoveItems, - ) - .await - .caused_by(trc::location!())? + && !resources.has_access_to_container( + access_token, + addressbook_id, + Acl::RemoveItems, + ) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } diff --git a/crates/dav/src/card/get.rs b/crates/dav/src/card/get.rs index 75946ad2..29796596 100644 --- a/crates/dav/src/card/get.rs +++ b/crates/dav/src/card/get.rs @@ -6,10 +6,13 @@ use common::{Server, auth::AccessToken}; use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime}; -use groupware::{contact::ContactCard, hierarchy::DavHierarchy}; +use groupware::{cache::GroupwareCache, contact::ContactCard}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use trc::AddContext; use crate::{ @@ -44,12 +47,11 @@ impl CardGetRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(access_token, account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, SyncCollection::AddressBook) .await .caused_by(trc::location!())?; let resource = resources - .paths - .by_name( + .by_path( resource_ .resource .ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?, @@ -61,23 +63,18 @@ impl CardGetRequestHandler for Server { // Validate ACL if !access_token.is_member(account_id) - && !self - .has_access_to_document( - access_token, - account_id, - Collection::AddressBook, - resource.parent_id.unwrap(), - Acl::ReadItems, - ) - .await - .caused_by(trc::location!())? + && !resources.has_access_to_container( + access_token, + resource.parent_id().unwrap(), + Acl::ReadItems, + ) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } // Fetch card let card_ = self - .get_archive(account_id, Collection::ContactCard, resource.document_id) + .get_archive(account_id, Collection::ContactCard, resource.document_id()) .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; @@ -93,7 +90,7 @@ impl CardGetRequestHandler for Server { vec![ResourceState { account_id, collection: Collection::ContactCard, - document_id: resource.document_id.into(), + document_id: resource.document_id().into(), etag: etag.clone().into(), path: resource_.resource.unwrap(), ..Default::default() diff --git a/crates/dav/src/card/mkcol.rs b/crates/dav/src/card/mkcol.rs index aeba96aa..e3e2b607 100644 --- a/crates/dav/src/card/mkcol.rs +++ b/crates/dav/src/card/mkcol.rs @@ -18,10 +18,10 @@ use dav_proto::{ RequestHeaders, Return, schema::{Namespace, request::MkCol, response::MkColResponse}, }; -use groupware::{contact::AddressBook, hierarchy::DavHierarchy}; +use groupware::{cache::GroupwareCache, contact::AddressBook}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::collection::Collection; +use jmap_proto::types::collection::{Collection, SyncCollection}; use store::write::BatchBuilder; use trc::AddContext; @@ -54,11 +54,11 @@ impl CardMkColRequestHandler for Server { return Err(DavError::Code(StatusCode::FORBIDDEN)); } else if name.contains('/') || self - .fetch_dav_resources(access_token, account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, SyncCollection::AddressBook) .await .caused_by(trc::location!())? - .paths - .by_name(name) + + .by_path(name) .is_some() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); diff --git a/crates/dav/src/card/mod.rs b/crates/dav/src/card/mod.rs index c58956bb..dc46cd18 100644 --- a/crates/dav/src/card/mod.rs +++ b/crates/dav/src/card/mod.rs @@ -92,11 +92,8 @@ pub(crate) async fn assert_is_unique_uid( .await .caused_by(trc::location!())?; if !hits.results.is_empty() { - for path in resources.paths.iter() { - if !path.is_container() - && hits.results.contains(path.document_id) - && path.parent_id.unwrap() == addressbook_id - { + for path in resources.children(addressbook_id) { + if hits.results.contains(path.document_id()) { return Err(DavError::Condition(DavErrorCondition::new( StatusCode::PRECONDITION_FAILED, CardCondition::NoUidConflict(resources.format_resource(path).into()), diff --git a/crates/dav/src/card/proppatch.rs b/crates/dav/src/card/proppatch.rs index 31b74a14..c2fac0f4 100644 --- a/crates/dav/src/card/proppatch.rs +++ b/crates/dav/src/card/proppatch.rs @@ -15,12 +15,15 @@ use dav_proto::{ }, }; use groupware::{ + cache::GroupwareCache, contact::{AddressBook, ContactCard}, - hierarchy::DavHierarchy, }; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use store::write::BatchBuilder; use trc::AddContext; @@ -73,14 +76,14 @@ impl CardPropPatchRequestHandler for Server { let uri = headers.uri; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(access_token, account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, SyncCollection::AddressBook) .await .caused_by(trc::location!())?; let resource = resource_ .resource - .and_then(|r| resources.paths.by_name(r)) + .and_then(|r| resources.by_path(r)) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - let document_id = resource.document_id; + let document_id = resource.document_id(); let collection = if resource.is_container() { Collection::AddressBook } else { @@ -94,22 +97,12 @@ impl CardPropPatchRequestHandler for Server { // Verify ACL if !access_token.is_member(account_id) { let (acl, document_id) = if resource.is_container() { - (Acl::Modify, resource.document_id) + (Acl::Modify, resource.document_id()) } else { - (Acl::ModifyItems, resource.parent_id.unwrap()) + (Acl::ModifyItems, resource.parent_id().unwrap()) }; - if !self - .has_access_to_document( - access_token, - account_id, - Collection::AddressBook, - document_id, - acl, - ) - .await - .caused_by(trc::location!())? - { + if !resources.has_access_to_container(access_token, document_id, acl) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } } diff --git a/crates/dav/src/card/query.rs b/crates/dav/src/card/query.rs index 6babb2c2..596df88e 100644 --- a/crates/dav/src/card/query.rs +++ b/crates/dav/src/card/query.rs @@ -23,10 +23,10 @@ use dav_proto::{ request::{AddressbookQuery, Filter, FilterOp, VCardPropertyWithGroup}, }, }; -use groupware::hierarchy::DavHierarchy; +use groupware::cache::GroupwareCache; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{acl::Acl, collection::SyncCollection}; use std::fmt::Write; use trc::AddContext; @@ -53,12 +53,11 @@ impl CardQueryRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(access_token, account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, SyncCollection::AddressBook) .await .caused_by(trc::location!())?; let resource = resources - .paths - .by_name( + .by_path( resource_ .resource .ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?, @@ -70,26 +69,19 @@ impl CardQueryRequestHandler for Server { // Obtain shared ids let shared_ids = if !access_token.is_member(account_id) { - self.shared_containers( - access_token, - account_id, - Collection::AddressBook, - [Acl::ReadItems], - false, - ) - .await - .caused_by(trc::location!())? - .into() + resources + .shared_containers(access_token, [Acl::ReadItems], false) + .into() } else { None }; // Obtain document ids in folder let mut items = Vec::with_capacity(16); - for resource in resources.children(resource.document_id) { + for resource in resources.children(resource.document_id()) { if shared_ids .as_ref() - .is_none_or(|ids| ids.contains(resource.document_id)) + .is_none_or(|ids| ids.contains(resource.document_id())) { items.push(PropFindItem::new( resources.format_resource(resource), diff --git a/crates/dav/src/card/update.rs b/crates/dav/src/card/update.rs index d6ad2199..e38f09ad 100644 --- a/crates/dav/src/card/update.rs +++ b/crates/dav/src/card/update.rs @@ -5,15 +5,18 @@ */ use calcard::{Entry, Parser}; -use common::{Server, auth::AccessToken}; +use common::{DavName, Server, auth::AccessToken}; use dav_proto::{ RequestHeaders, Return, schema::{property::Rfc1123DateTime, response::CardCondition}, }; -use groupware::{DavName, contact::ContactCard, hierarchy::DavHierarchy}; +use groupware::{cache::GroupwareCache, contact::ContactCard}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use store::write::BatchBuilder; use trc::AddContext; @@ -54,7 +57,7 @@ impl CardUpdateRequestHandler for Server { .into_owned_uri()?; let account_id = resource.account_id; let resources = self - .fetch_dav_resources(access_token, account_id, Collection::AddressBook) + .fetch_dav_resources(access_token, account_id, SyncCollection::AddressBook) .await .caused_by(trc::location!())?; let resource_name = resource @@ -84,25 +87,16 @@ impl CardUpdateRequestHandler for Server { } }; - if let Some(resource) = resources.paths.by_name(resource_name) { + if let Some(resource) = resources.by_path(resource_name) { if resource.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } // Validate ACL - let parent_id = resource.parent_id.unwrap(); - let document_id = resource.document_id; + let parent_id = resource.parent_id().unwrap(); + let document_id = resource.document_id(); if !access_token.is_member(account_id) - && !self - .has_access_to_document( - access_token, - account_id, - Collection::AddressBook, - parent_id, - Acl::ModifyItems, - ) - .await - .caused_by(trc::location!())? + && !resources.has_access_to_container(access_token, parent_id, Acl::ModifyItems) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -197,16 +191,11 @@ impl CardUpdateRequestHandler for Server { // Validate ACL if !access_token.is_member(account_id) - && !self - .has_access_to_document( - access_token, - account_id, - Collection::AddressBook, - parent.document_id, - Acl::AddItems, - ) - .await - .caused_by(trc::location!())? + && !resources.has_access_to_container( + access_token, + parent.document_id(), + Acl::AddItems, + ) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -241,7 +230,7 @@ impl CardUpdateRequestHandler for Server { self, &resources, account_id, - parent.document_id, + parent.document_id(), vcard.uid(), ) .await?; @@ -250,7 +239,7 @@ impl CardUpdateRequestHandler for Server { let card = ContactCard { names: vec![DavName { name: name.to_string(), - parent_id: parent.document_id, + parent_id: parent.document_id(), }], card: vcard, size: bytes.len() as u32, diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index f42d86c0..775384e8 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -8,7 +8,7 @@ use crate::{ DavError, DavErrorCondition, DavResourceName, common::uri::DavUriResource, principal::propfind::PrincipalPropFind, }; -use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; +use common::{DavResources, Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::{ RequestHeaders, schema::{ @@ -18,9 +18,7 @@ use dav_proto::{ }, }; use directory::{QueryBy, Type, backend::internal::manage::ManageDirectory}; -use groupware::{ - calendar::Calendar, contact::AddressBook, file::FileNode, hierarchy::DavHierarchy, -}; +use groupware::{cache::GroupwareCache, calendar::Calendar, contact::AddressBook, file::FileNode}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{ @@ -58,25 +56,6 @@ pub(crate) trait DavAclHandler: Sync + Send { collection: Collection, ) -> impl Future>> + Send; - fn validate_and_map_parent_acl( - &self, - access_token: &AccessToken, - account_id: u32, - collection: Collection, - parent_id: Option, - check_acls: impl Into> + Send, - ) -> impl Future> + Send; - - #[allow(clippy::too_many_arguments)] - fn validate_acl( - &self, - access_token: &AccessToken, - account_id: u32, - collection: Collection, - document_id: u32, - acl: impl Into> + Send, - ) -> impl Future> + Send; - fn resolve_ace( &self, access_token: &AccessToken, @@ -86,6 +65,16 @@ pub(crate) trait DavAclHandler: Sync + Send { ) -> impl Future>> + Send; } +pub(crate) trait ResourceAcl { + fn validate_and_map_parent_acl( + &self, + access_token: &AccessToken, + is_member: bool, + parent_id: Option, + check_acls: impl Into> + Send, + ) -> crate::Result; +} + impl DavAclHandler for Server { async fn handle_acl_request( &self, @@ -108,20 +97,20 @@ impl DavAclHandler for Server { return Err(DavError::Code(StatusCode::FORBIDDEN)); } let resources = self - .fetch_dav_resources(access_token, account_id, collection) + .fetch_dav_resources(access_token, account_id, collection.into()) .await .caused_by(trc::location!())?; let resource = resource_ .resource - .and_then(|r| resources.paths.by_name(r)) + .and_then(|r| resources.by_path(r)) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - if !resource.is_container() && !matches!(collection, Collection::FileNode) { + if !resource.resource.is_container() && !matches!(collection, Collection::FileNode) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } // Fetch node let archive = self - .get_archive(account_id, collection, resource.document_id) + .get_archive(account_id, collection, resource.document_id()) .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; @@ -158,7 +147,7 @@ impl DavAclHandler for Server { access_token, calendar, account_id, - resource.document_id, + resource.document_id(), &mut batch, ) .caused_by(trc::location!())?; @@ -173,7 +162,7 @@ impl DavAclHandler for Server { access_token, book, account_id, - resource.document_id, + resource.document_id(), &mut batch, ) .caused_by(trc::location!())?; @@ -187,7 +176,7 @@ impl DavAclHandler for Server { access_token, node, account_id, - resource.document_id, + resource.document_id(), &mut batch, ) .caused_by(trc::location!())?; @@ -412,63 +401,6 @@ impl DavAclHandler for Server { Ok(grants) } - async fn validate_and_map_parent_acl( - &self, - access_token: &AccessToken, - account_id: u32, - collection: Collection, - parent_id: Option, - check_acls: impl Into> + Send, - ) -> crate::Result { - match parent_id { - Some(parent_id) => { - if access_token.is_member(account_id) - || self - .has_access_to_document( - access_token, - account_id, - collection, - parent_id, - check_acls, - ) - .await - .caused_by(trc::location!())? - { - Ok(parent_id + 1) - } else { - Err(DavError::Code(StatusCode::FORBIDDEN)) - } - } - None => { - if access_token.is_member(account_id) { - Ok(0) - } else { - Err(DavError::Code(StatusCode::FORBIDDEN)) - } - } - } - } - - async fn validate_acl( - &self, - access_token: &AccessToken, - account_id: u32, - collection: Collection, - document_id: u32, - acl: impl Into> + Send, - ) -> crate::Result<()> { - if access_token.is_member(account_id) - || self - .has_access_to_document(access_token, account_id, collection, document_id, acl) - .await - .caused_by(trc::location!())? - { - Ok(()) - } else { - Err(DavError::Code(StatusCode::FORBIDDEN)) - } - } - async fn resolve_ace( &self, access_token: &AccessToken, @@ -523,6 +455,33 @@ impl DavAclHandler for Server { } } +impl ResourceAcl for DavResources { + fn validate_and_map_parent_acl( + &self, + access_token: &AccessToken, + is_member: bool, + parent_id: Option, + check_acls: impl Into> + Send, + ) -> crate::Result { + match parent_id { + Some(parent_id) => { + if is_member || self.has_access_to_container(access_token, parent_id, check_acls) { + Ok(parent_id + 1) + } else { + Err(DavError::Code(StatusCode::FORBIDDEN)) + } + } + None => { + if is_member { + Ok(0) + } else { + Err(DavError::Code(StatusCode::FORBIDDEN)) + } + } + } + } +} + pub(crate) trait Privileges { fn current_privilege_set( &self, diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs index 8b2b2cd9..9e3fd71c 100644 --- a/crates/dav/src/common/lock.rs +++ b/crates/dav/src/common/lock.rs @@ -11,7 +11,8 @@ use dav_proto::schema::request::{DavPropertyValue, DeadProperty}; use dav_proto::schema::response::{BaseCondition, List, PropResponse}; use dav_proto::{Condition, Depth, Timeout}; use dav_proto::{RequestHeaders, schema::request::LockInfo}; -use groupware::hierarchy::DavHierarchy; + +use groupware::cache::GroupwareCache; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::collection::Collection; @@ -553,22 +554,16 @@ impl LockRequestHandler for Server { // Fetch sync token if needs_sync_token && resource_state.sync_token.is_none() { - let change_id = self + let id = self .fetch_dav_resources( access_token, resource_state.account_id, - resource_state.collection.main_collection(), + resource_state.collection.into(), ) .await .caused_by(trc::location!())? - .modseq; - resource_state.sync_token = Some( - Urn::Sync { - id: change_id.unwrap_or_default(), - seq: 0, - } - .to_string(), - ); + .highest_change_id; + resource_state.sync_token = Some(Urn::Sync { id, seq: 0 }.to_string()); } } diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index bf451d6a..e17038e7 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -26,7 +26,7 @@ use crate::{ }; use calcard::common::timezone::Tz; use common::{ - DavResource, DavResources, Server, + DavResourcePath, DavResources, Server, auth::{AccessToken, AsTenantId}, }; use dav_proto::{ @@ -48,11 +48,14 @@ use dav_proto::{ }; use directory::{Type, backend::internal::manage::ManageDirectory}; use groupware::{ - DavCalendarResource, DavResourceName, calendar::ArchivedTimezone, hierarchy::DavHierarchy, + DavCalendarResource, DavResourceName, cache::GroupwareCache, calendar::ArchivedTimezone, }; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use percent_encoding::NON_ALPHANUMERIC; use std::sync::Arc; use store::{ @@ -90,7 +93,7 @@ pub(crate) struct PropFindData { #[derive(Default)] pub(crate) struct PropFindAccountData { - pub sync_token: Option, + pub resources: Option>, pub quota: Option, pub owner: Option, pub locks: Option>, @@ -350,7 +353,7 @@ impl PropFindRequestHandler for Server { let mut data = PropFindData::new(); let collection_container; let collection_children; - let mut ctag = None; + let sync_collection; let mut paths; let mut query_filter = None; let mut limit = std::cmp::min( @@ -366,30 +369,27 @@ impl PropFindRequestHandler for Server { let account_id = resource.account_id; collection_container = resource.collection; collection_children = collection_container.child_collection().unwrap(); + sync_collection = SyncCollection::from(collection_container); let container_has_children = collection_children != collection_container; - let resources = self - .fetch_dav_resources(access_token, account_id, collection_container) + let resources = data + .resources(self, access_token, account_id, sync_collection) .await .caused_by(trc::location!())?; response.set_namespace(collection_container.namespace()); - ctag = Some(resources.modseq.unwrap_or_default()); // Obtain document ids let mut display_containers = if !access_token.is_member(account_id) { - self.shared_containers( - access_token, - account_id, - collection_container, - [if container_has_children { - Acl::ReadItems - } else { - Acl::Read - }], - true, - ) - .await - .caused_by(trc::location!())? - .into() + resources + .shared_containers( + access_token, + [if container_has_children { + Acl::ReadItems + } else { + Acl::Read + }], + true, + ) + .into() } else { None }; @@ -397,9 +397,9 @@ impl PropFindRequestHandler for Server { .as_ref() .filter(|_| container_has_children) .map(|containers| { - RoaringBitmap::from_iter(resources.paths.iter().filter_map(|r| { - if r.parent_id - .is_some_and(|parent_id| containers.contains(parent_id)) + RoaringBitmap::from_iter(resources.resources.iter().filter_map(|r| { + if r.child_names() + .is_some_and(|n| n.iter().any(|n| containers.contains(n.parent_id))) { Some(r.document_id) } else { @@ -411,47 +411,58 @@ impl PropFindRequestHandler for Server { // Filter by changelog match query.sync_type { SyncType::From { id, seq } => { - let todo = "fix"; - let container_changes = self + let changes = self .store() - .changes(account_id, collection_container, Query::Since(id)) + .changes(account_id, sync_collection, Query::Since(id)) .await .caused_by(trc::location!())?; - let children_changes = if container_has_children { - self.store() - .changes(account_id, collection_children, Query::Since(id)) - .await - .caused_by(trc::location!())? - .into() - } else { - None - }; // Merge changes let mut total_changes = 0; - for (changes, document_ids) in [ - Some((&container_changes, &mut display_containers)), - children_changes - .as_ref() - .map(|changes| (changes, &mut display_children)), - ] - .into_iter() - .flatten() - { + if container_has_children { + let mut container_changes = RoaringBitmap::new(); + let mut item_changes = RoaringBitmap::new(); + + for change in changes.changes { + match change { + Change::InsertItem(id) | Change::UpdateItem(id) => { + item_changes.insert(id as u32); + } + Change::InsertContainer(id) | Change::UpdateContainer(id) => { + container_changes.insert(id as u32); + } + _ => (), + } + } + + for (document_ids, changes) in [ + (&mut display_containers, container_changes), + (&mut display_children, item_changes), + ] { + 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); + } + } + } else { let changes = RoaringBitmap::from_iter( changes.changes.iter().filter_map(|change| match change { - Change::InsertItem(id) | Change::UpdateItem(id) => { - Some(*id as u32) - } + Change::InsertItem(id) + | Change::UpdateItem(id) + | Change::InsertContainer(id) + | Change::UpdateContainer(id) => Some(*id as u32), _ => None, }), ); - if let Some(document_ids) = document_ids { + if let Some(document_ids) = &mut display_containers { *document_ids &= changes; total_changes += document_ids.len() as usize; } else { total_changes += changes.len() as usize; - *document_ids = Some(changes); + display_containers = Some(changes); } } @@ -483,35 +494,11 @@ impl PropFindRequestHandler for Server { } 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); + response.set_sync_token(resources.sync_token()); } } SyncType::Initial => { - response.set_sync_token( - data.sync_token(self, account_id, collection_container) - .await - .caused_by(trc::location!())?, - ); + response.set_sync_token(resources.sync_token()); } SyncType::None => (), } @@ -523,16 +510,16 @@ impl PropFindRequestHandler for Server { display_containers.as_ref().is_none_or(|containers| { if container_has_children { if item.is_container() { - containers.contains(item.document_id) + containers.contains(item.document_id()) } else { display_children.as_ref().is_some_and(|children| { - children.contains(item.document_id) + children.contains(item.document_id()) }) } } else { - containers.contains(item.document_id) + containers.contains(item.document_id()) } - }) && (!query.depth_no_root || item.name != resource) + }) && (!query.depth_no_root || item.path() != resource) }) .map(|item| { PropFindItem::new(resources.format_resource(item), account_id, item) @@ -561,14 +548,14 @@ impl PropFindRequestHandler for Server { display_containers.as_ref().is_none_or(|containers| { if container_has_children { if item.is_container() { - containers.contains(item.document_id) + containers.contains(item.document_id()) } else { display_children.as_ref().is_some_and(|children| { - children.contains(item.document_id) + children.contains(item.document_id()) }) } } else { - containers.contains(item.document_id) + containers.contains(item.document_id()) } }) }) @@ -593,12 +580,11 @@ impl PropFindRequestHandler for Server { parent_collection, } => { paths = Vec::with_capacity(hrefs.len()); - let mut resources_by_account: AHashMap< - u32, - (Arc, Arc>), - > = AHashMap::with_capacity(3); + let mut shared_folders_by_account: AHashMap> = + AHashMap::with_capacity(3); collection_container = parent_collection; collection_children = collection_container.child_collection().unwrap(); + sync_collection = SyncCollection::from(collection_container); response.set_namespace(collection_container.namespace()); for item in hrefs { @@ -618,51 +604,38 @@ impl PropFindRequestHandler for Server { }; let account_id = resource.account_id; - let (resources, document_ids) = - if let Some(resources) = resources_by_account.get(&account_id) { - resources.clone() + let resources = data + .resources(self, access_token, account_id, sync_collection) + .await + .caused_by(trc::location!())?; + + let document_ids = if !access_token.is_member(account_id) { + if let Some(document_ids) = shared_folders_by_account.get(&account_id) { + document_ids.clone().into() } else { - let resources = self - .fetch_dav_resources(access_token, account_id, collection_container) - .await - .caused_by(trc::location!())?; - let document_ids = Arc::new(if !access_token.is_member(account_id) { - self.shared_containers( - access_token, - account_id, - collection_container, - [Acl::ReadItems], - false, - ) - .await - .caused_by(trc::location!())? - .into() - } else { - None - }); - resources_by_account - .insert(account_id, (resources.clone(), document_ids.clone())); - (resources, document_ids) - }; + let document_ids = Arc::new(resources.shared_containers( + access_token, + [if collection_children == collection_container { + Acl::ReadItems + } else { + Acl::Read + }], + true, + )); + shared_folders_by_account.insert(account_id, document_ids.clone()); + document_ids.into() + } + } else { + None + }; - /*let c = println!( - "resources: {:?} resource: {resource:?}", - resources - .paths - .iter() - .map(|r| r.name.to_string()) - .collect::>() - );*/ - - if let Some(resource) = resource - .resource - .and_then(|name| resources.paths.by_name(name)) + if let Some(resource) = + resource.resource.and_then(|name| resources.by_path(name)) { if !resource.is_container() { if document_ids .as_ref() - .as_ref() - .is_none_or(|docs| docs.contains(resource.document_id)) + .is_none_or(|docs| docs.contains(resource.document_id())) { paths.push(PropFindItem::new( resources.format_resource(resource), @@ -699,6 +672,7 @@ impl PropFindRequestHandler for Server { query_filter = Some(filter); collection_container = parent_collection; collection_children = collection_container.child_collection().unwrap(); + sync_collection = SyncCollection::from(collection_container); response.set_namespace(collection_container.namespace()); } DavQueryResource::None => unreachable!(), @@ -795,18 +769,19 @@ impl PropFindRequestHandler for Server { }, ArchivedResource::CalendarEvent(event), ) => { - let mut query_handler = CalendarQueryHandler::new( - event.inner, - *max_time_range, - try_parse_tz(timezone) - .or_else(|| { - item.parent_id.and_then(|calendar_id| { - self.cached_dav_resources(account_id, Collection::Calendar) - .and_then(|r| r.calendar_default_tz(calendar_id)) - }) - }) - .unwrap_or(Tz::UTC), - ); + let default_tz = if let Some(tz) = try_parse_tz(timezone) { + tz + } else if let Some(calendar_id) = item.parent_id { + data.resources(self, access_token, account_id, SyncCollection::Calendar) + .await + .caused_by(trc::location!())? + .calendar_default_tz(calendar_id) + .unwrap_or(Tz::UTC) + } else { + Tz::UTC + }; + let mut query_handler = + CalendarQueryHandler::new(event.inner, *max_time_range, default_tz); if !query_handler.filter(event.inner, filter) { continue; } @@ -872,15 +847,12 @@ impl PropFindRequestHandler for Server { } WebDavProperty::GetCTag => { if item.is_container { - let ctag = if let Some(ctag) = ctag { - ctag - } else { - let todo = "fix"; - self.store() - .get_last_change_id(account_id, collection) - .await? - .unwrap_or_default() - }; + let ctag = data + .resources(self, access_token, account_id, sync_collection) + .await + .caused_by(trc::location!())? + .highest_change_id; + fields.push(DavPropertyValue::new( property.clone(), DavValue::String(format!("\"{ctag}\"")), @@ -928,12 +900,13 @@ impl PropFindRequestHandler for Server { } } WebDavProperty::SyncToken => { - fields.push(DavPropertyValue::new( - property.clone(), - data.sync_token(self, account_id, collection_children) - .await - .caused_by(trc::location!())?, - )); + let sync_token = data + .resources(self, access_token, account_id, sync_collection) + .await + .caused_by(trc::location!())? + .sync_token(); + + fields.push(DavPropertyValue::new(property.clone(), sync_token)); } WebDavProperty::CurrentUserPrincipal => { if !query.expand { @@ -1085,14 +1058,10 @@ impl PropFindRequestHandler for Server { ) } else if let Some(parent_id) = item.parent_id { current_user_privilege_set( - self.document_acl( - access_token.primary_id(), - account_id, - collection_container, - parent_id, - ) - .await - .caused_by(trc::location!())?, + data.resources(self, access_token, account_id, sync_collection) + .await + .caused_by(trc::location!())? + .container_acl(access_token, parent_id), ) } else { vec![] @@ -1427,12 +1396,12 @@ impl PropFindRequestHandler for Server { } impl PropFindItem { - pub fn new(name: String, account_id: u32, resource: &DavResource) -> Self { + pub fn new(name: String, account_id: u32, resource: DavResourcePath<'_>) -> Self { Self { name, account_id, - document_id: resource.document_id, - parent_id: resource.parent_id, + document_id: resource.document_id(), + parent_id: resource.parent_id(), is_container: resource.is_container(), } } @@ -1479,26 +1448,24 @@ impl PropFindData { Ok(data.owner.clone().unwrap()) } - pub async fn sync_token( + pub async fn resources( &mut self, server: &Server, + access_token: &AccessToken, account_id: u32, - collection_children: Collection, - ) -> trc::Result { + sync_collection: SyncCollection, + ) -> trc::Result> { let data = self.accounts.entry(account_id).or_default(); - if data.sync_token.is_none() { - let todo = "fix"; - let id = server - .store() - .get_last_change_id(account_id, collection_children) + if data.resources.is_none() { + let resources = server + .fetch_dav_resources(access_token, account_id, sync_collection) .await - .caused_by(trc::location!())? - .unwrap_or_default(); - data.sync_token = Urn::Sync { id, seq: 0 }.to_string().into(); + .caused_by(trc::location!())?; + data.resources = resources.into(); } - Ok(data.sync_token.clone().unwrap()) + Ok(data.resources.clone().unwrap()) } pub async fn locks( @@ -1538,3 +1505,17 @@ impl PropFindData { } } } + +pub(crate) trait SyncTokenUrn { + fn sync_token(&self) -> String; +} + +impl SyncTokenUrn for DavResources { + fn sync_token(&self) -> String { + Urn::Sync { + id: self.highest_change_id, + seq: 0, + } + .to_string() + } +} diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index e296e019..9a25b40c 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -9,7 +9,8 @@ use std::fmt::Display; use common::{Server, auth::AccessToken}; use directory::backend::internal::manage::ManageDirectory; -use groupware::hierarchy::DavHierarchy; + +use groupware::cache::GroupwareCache; use http_proto::request::decode_path_element; use hyper::StatusCode; use jmap_proto::types::collection::Collection; @@ -128,11 +129,10 @@ impl DavUriResource for Server { ) -> trc::Result> { if let Some(resource) = uri.resource { if let Some(resource) = self - .fetch_dav_resources(access_token, uri.account_id, uri.collection) + .fetch_dav_resources(access_token, uri.account_id, uri.collection.into()) .await .caused_by(trc::location!())? - .paths - .by_name(resource) + .by_path(resource) { Ok(Some(DocumentUri { collection: if resource.is_container() || uri.collection == Collection::FileNode @@ -144,7 +144,7 @@ impl DavUriResource for Server { Collection::ContactCard }, account_id: uri.account_id, - resource: resource.document_id, + resource: resource.document_id(), })) } else { Ok(None) diff --git a/crates/dav/src/file/copy_move.rs b/crates/dav/src/file/copy_move.rs index a4093540..dad83609 100644 --- a/crates/dav/src/file/copy_move.rs +++ b/crates/dav/src/file/copy_move.rs @@ -9,18 +9,22 @@ use crate::{ DavError, DavMethod, common::{ ExtractETag, - acl::DavAclHandler, lock::{LockRequestHandler, ResourceState}, uri::{DavUriResource, UriResource}, }, file::{DavFileResource, FileItemId}, }; -use common::{DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use common::{ + DavResourcePath, DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder, +}; use dav_proto::{Depth, RequestHeaders}; -use groupware::{DestroyArchive, file::FileNode, hierarchy::DavHierarchy}; +use groupware::{DestroyArchive, cache::GroupwareCache, file::FileNode}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use std::sync::Arc; use store::{ ahash::AHashMap, @@ -50,32 +54,27 @@ impl FileCopyMoveRequestHandler for Server { .await? .into_owned_uri()?; let from_account_id = from_resource_.account_id; - let from_files = self - .fetch_dav_resources(access_token, from_account_id, Collection::FileNode) + let from_resources = self + .fetch_dav_resources(access_token, from_account_id, SyncCollection::FileNode) .await .caused_by(trc::location!())?; - let from_resource = from_files.map_resource::(&from_resource_)?; + let from_resource = from_resources.map_resource::(&from_resource_)?; let from_resource_name = from_resource_.resource.unwrap(); // Validate source ACLs if !access_token.is_member(from_account_id) { - let shared = self - .shared_containers( - access_token, - from_account_id, - Collection::FileNode, - if is_move { - [Acl::Read, Acl::Delete].as_slice().iter().copied() - } else { - [Acl::Read].as_slice().iter().copied() - }, - false, - ) - .await - .caused_by(trc::location!())?; + let shared = from_resources.shared_containers( + access_token, + if is_move { + [Acl::Read, Acl::Delete].as_slice().iter().copied() + } else { + [Acl::Read].as_slice().iter().copied() + }, + false, + ); - for resource in from_files.subtree(from_resource_.resource.unwrap()) { - if !shared.contains(resource.document_id) { + for resource in from_resources.subtree(from_resource_.resource.unwrap()) { + if !shared.contains(resource.document_id()) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } } @@ -97,10 +96,10 @@ impl FileCopyMoveRequestHandler for Server { let to_account_id = destination .account_id .ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?; - let to_files = if to_account_id == from_account_id { - from_files.clone() + let to_resources = if to_account_id == from_account_id { + from_resources.clone() } else { - self.fetch_dav_resources(access_token, to_account_id, Collection::FileNode) + self.fetch_dav_resources(access_token, to_account_id, SyncCollection::FileNode) .await .caused_by(trc::location!())? }; @@ -120,29 +119,29 @@ impl FileCopyMoveRequestHandler for Server { let mut delete_destination = None; // Check if the resource exists - let mut destination = - if let Some((destination, new_name)) = to_files.map_parent(destination_resource_name) { - if let Some(mut existing_destination) = to_files - .paths - .by_name(destination_resource_name) - .map(Destination::from_dav_resource) - { - if !headers.overwrite_fail { - existing_destination.account_id = to_account_id; - delete_destination = Some(existing_destination); - } else { - return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)); - } + let mut destination = if let Some((destination, new_name)) = + to_resources.map_parent(destination_resource_name) + { + if let Some(mut existing_destination) = to_resources + .by_path(destination_resource_name) + .map(Destination::from_dav_resource) + { + if !headers.overwrite_fail { + existing_destination.account_id = to_account_id; + delete_destination = Some(existing_destination); + } else { + return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)); } + } - let mut destination = destination - .map(Destination::from_dav_resource) - .unwrap_or_default(); - destination.new_name = Some(new_name.to_string()); - destination - } else { - return Err(DavError::Code(StatusCode::CONFLICT)); - }; + let mut destination = destination + .map(Destination::from_dav_resource) + .unwrap_or_default(); + destination.new_name = Some(new_name.to_string()); + destination + } else { + return Err(DavError::Code(StatusCode::CONFLICT)); + }; destination.account_id = to_account_id; if delete_destination.is_none() @@ -158,24 +157,22 @@ impl FileCopyMoveRequestHandler for Server { // Validate destination ACLs if let Some(document_id) = destination.document_id { if let Some(delete_destination) = &delete_destination { - self.validate_acl( - access_token, - to_account_id, - Collection::FileNode, - delete_destination.document_id.unwrap(), - Acl::Delete, - ) - .await?; + if !access_token.is_member(to_account_id) + && !from_resources.has_access_to_container( + access_token, + delete_destination.document_id.unwrap(), + Acl::Delete, + ) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } } - self.validate_acl( - access_token, - to_account_id, - Collection::FileNode, - document_id, - Acl::Modify, - ) - .await?; + if !access_token.is_member(to_account_id) + && !from_resources.has_access_to_container(access_token, document_id, Acl::Modify) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } } else if !access_token.is_member(to_account_id) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -217,12 +214,8 @@ impl FileCopyMoveRequestHandler for Server { // Validate quota if !is_move || from_account_id != to_account_id { - let res = from_files - .paths - .by_id(from_resource.resource.document_id) - .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - let space_needed = from_files - .subtree(&res.name) + let space_needed = from_resources + .subtree(from_resource_name) .map(|a| a.size() as u64) .sum::(); self.has_available_quota( @@ -239,13 +232,13 @@ impl FileCopyMoveRequestHandler for Server { if is_overwrite { delete_destination = None; // Find ids to delete - let mut ids = to_files + let mut ids = to_resources .subtree(destination_resource_name) .collect::>(); if !ids.is_empty() { - ids.sort_unstable_by_key(|b| std::cmp::Reverse(b.hierarchy_sequence())); + ids.sort_unstable_by_key(|b| std::cmp::Reverse(b.hierarchy_seq())); let mut sorted_ids = Vec::with_capacity(ids.len()); - sorted_ids.extend(ids.into_iter().map(|a| a.document_id)); + sorted_ids.extend(ids.into_iter().map(|a| a.document_id())); DestroyArchive(sorted_ids) .delete(self, access_token, destination.account_id) .await @@ -258,8 +251,9 @@ impl FileCopyMoveRequestHandler for Server { move_container( self, access_token, - from_files, + from_resources, from_resource, + from_resource_name, destination, headers.depth, ) @@ -269,8 +263,9 @@ impl FileCopyMoveRequestHandler for Server { copy_container( self, access_token, - from_files, + from_resources, from_resource, + from_resource_name, destination, headers.depth, false, @@ -309,7 +304,6 @@ pub(crate) struct Destination { pub account_id: u32, pub new_name: Option, pub document_id: Option, - //pub parent_id: Option, pub is_container: bool, } @@ -328,8 +322,9 @@ impl Default for Destination { async fn move_container( server: &Server, access_token: &AccessToken, - from_files: Arc, + from_resources: Arc, from_resource: UriResource, + from_resource_name: &str, destination: Destination, depth: Depth, ) -> crate::Result { @@ -373,8 +368,9 @@ async fn move_container( copy_container( server, access_token, - from_files, + from_resources, from_resource, + from_resource_name, destination, depth, true, @@ -383,11 +379,13 @@ async fn move_container( } } +#[allow(clippy::too_many_arguments)] async fn copy_container( server: &Server, access_token: &AccessToken, - from_files: Arc, + from_resources: Arc, from_resource: UriResource, + from_resource_name: &str, mut destination: Destination, depth: Depth, delete_source: bool, @@ -402,23 +400,18 @@ async fn copy_container( let from_account_id = from_resource.account_id; let to_account_id = destination.account_id; - let from_document_id = from_resource.resource.document_id; let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0); // Obtain files to copy - let res = from_files - .paths - .by_id(from_document_id) - .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; let mut copy_files = if infinity_copy { - from_files - .subtree(&res.name) - .map(|r| (r.document_id, r.hierarchy_sequence())) + from_resources + .subtree(from_resource_name) + .map(|r| (r.document_id(), r.hierarchy_seq())) .collect::>() } else { - from_files - .subtree_with_depth(&res.name, 1) - .map(|r| (r.document_id, r.hierarchy_sequence())) + from_resources + .subtree_with_depth(from_resource_name, 1) + .map(|r| (r.document_id(), r.hierarchy_seq())) .collect::>() }; @@ -777,10 +770,10 @@ async fn rename_item( } impl FromDavResource for Destination { - fn from_dav_resource(item: &common::DavResource) -> Self { + fn from_dav_resource(item: DavResourcePath<'_>) -> Self { Destination { account_id: u32::MAX, - document_id: Some(item.document_id), + document_id: Some(item.document_id()), is_container: item.is_container(), new_name: None, } diff --git a/crates/dav/src/file/delete.rs b/crates/dav/src/file/delete.rs index d6ac2fbb..7117306f 100644 --- a/crates/dav/src/file/delete.rs +++ b/crates/dav/src/file/delete.rs @@ -4,14 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; -use dav_proto::RequestHeaders; -use groupware::{DestroyArchive, hierarchy::DavHierarchy}; -use http_proto::HttpResponse; -use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; -use trc::AddContext; - use crate::{ DavError, DavMethod, common::{ @@ -19,6 +11,13 @@ use crate::{ uri::DavUriResource, }, }; +use common::{Server, auth::AccessToken}; +use dav_proto::RequestHeaders; +use groupware::{DestroyArchive, cache::GroupwareCache}; +use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::{acl::Acl, collection::SyncCollection}; +use trc::AddContext; pub(crate) trait FileDeleteRequestHandler: Sync + Send { fn handle_file_delete_request( @@ -44,35 +43,26 @@ impl FileDeleteRequestHandler for Server { .resource .filter(|r| !r.is_empty()) .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; - let files = self - .fetch_dav_resources(access_token, account_id, Collection::FileNode) + let resources = self + .fetch_dav_resources(access_token, account_id, SyncCollection::FileNode) .await .caused_by(trc::location!())?; // Find ids to delete - let mut ids = files.subtree(delete_path).collect::>(); + let mut ids = resources.subtree(delete_path).collect::>(); if ids.is_empty() { return Err(DavError::Code(StatusCode::NOT_FOUND)); } // Sort ids descending from the deepest to the root - ids.sort_unstable_by_key(|b| std::cmp::Reverse(b.hierarchy_sequence())); - let document_id = ids.last().map(|a| a.document_id).unwrap(); + ids.sort_unstable_by_key(|b| std::cmp::Reverse(b.hierarchy_seq())); + let document_id = ids.last().map(|a| a.document_id()).unwrap(); let mut sorted_ids = Vec::with_capacity(ids.len()); - sorted_ids.extend(ids.into_iter().map(|a| a.document_id)); + sorted_ids.extend(ids.into_iter().map(|a| a.document_id())); // Validate ACLs if !access_token.is_member(account_id) { - let permissions = self - .shared_containers( - access_token, - account_id, - Collection::FileNode, - [Acl::Delete], - false, - ) - .await - .caused_by(trc::location!())?; + let permissions = resources.shared_containers(access_token, [Acl::Delete], false); if permissions.len() != sorted_ids.len() as u64 || !sorted_ids.iter().all(|id| permissions.contains(*id)) { diff --git a/crates/dav/src/file/get.rs b/crates/dav/src/file/get.rs index 401c33c5..c5696eb0 100644 --- a/crates/dav/src/file/get.rs +++ b/crates/dav/src/file/get.rs @@ -6,10 +6,10 @@ use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime}; -use groupware::{file::FileNode, hierarchy::DavHierarchy}; +use groupware::{cache::GroupwareCache, file::FileNode}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{acl::Acl, collection::{Collection, SyncCollection}}; use trc::AddContext; use crate::{ @@ -45,7 +45,7 @@ impl FileGetRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let files = self - .fetch_dav_resources(access_token, account_id, Collection::FileNode) + .fetch_dav_resources(access_token, account_id, SyncCollection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_resource(&resource_)?; diff --git a/crates/dav/src/file/mkcol.rs b/crates/dav/src/file/mkcol.rs index 01acaace..970a8352 100644 --- a/crates/dav/src/file/mkcol.rs +++ b/crates/dav/src/file/mkcol.rs @@ -4,30 +4,31 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; -use dav_proto::{ - RequestHeaders, Return, - schema::{Namespace, request::MkCol, response::MkColResponse}, -}; -use groupware::{file::FileNode, hierarchy::DavHierarchy}; -use http_proto::HttpResponse; -use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; -use store::write::{BatchBuilder, now}; -use trc::AddContext; - +use super::proppatch::FilePropPatchRequestHandler; use crate::{ DavMethod, PropStatBuilder, common::{ ExtractETag, - acl::DavAclHandler, + acl::ResourceAcl, lock::{LockRequestHandler, ResourceState}, uri::DavUriResource, }, file::DavFileResource, }; - -use super::proppatch::FilePropPatchRequestHandler; +use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use dav_proto::{ + RequestHeaders, Return, + schema::{Namespace, request::MkCol, response::MkColResponse}, +}; +use groupware::{cache::GroupwareCache, file::FileNode}; +use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; +use store::write::{BatchBuilder, now}; +use trc::AddContext; pub(crate) trait FileMkColRequestHandler: Sync + Send { fn handle_file_mkcol_request( @@ -51,22 +52,19 @@ impl FileMkColRequestHandler for Server { .await? .into_owned_uri()?; let account_id = resource_.account_id; - let files = self - .fetch_dav_resources(access_token, account_id, Collection::FileNode) + let resources = self + .fetch_dav_resources(access_token, account_id, SyncCollection::FileNode) .await .caused_by(trc::location!())?; - let resource = files.map_parent_resource(&resource_)?; + let resource = resources.map_parent_resource(&resource_)?; // Validate and map parent ACL - let parent_id = self - .validate_and_map_parent_acl( - access_token, - account_id, - Collection::FileNode, - resource.resource.0, - Acl::AddItems, - ) - .await?; + let parent_id = resources.validate_and_map_parent_acl( + access_token, + access_token.is_member(account_id), + resource.resource.0, + Acl::AddItems, + )?; // Validate headers self.validate_headers( diff --git a/crates/dav/src/file/mod.rs b/crates/dav/src/file/mod.rs index c447f110..60f3f58a 100644 --- a/crates/dav/src/file/mod.rs +++ b/crates/dav/src/file/mod.rs @@ -4,14 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{DavResource, DavResources}; -use dav_proto::schema::property::{DavProperty, WebDavProperty}; -use hyper::StatusCode; - use crate::{ DavError, common::uri::{OwnedUri, UriResource}, }; +use common::{DavResourcePath, DavResources}; +use dav_proto::schema::property::{DavProperty, WebDavProperty}; +use hyper::StatusCode; pub mod copy_move; pub mod delete; @@ -65,7 +64,7 @@ pub(crate) static FILE_ITEM_PROPS: [DavProperty; 19] = [ ]; pub(crate) trait FromDavResource { - fn from_dav_resource(item: &DavResource) -> Self; + fn from_dav_resource(item: DavResourcePath<'_>) -> Self; } pub(crate) struct FileItemId { @@ -80,7 +79,7 @@ pub(crate) trait DavFileResource { resource: &OwnedUri<'_>, ) -> crate::Result>; - fn map_parent<'x>(&self, resource: &'x str) -> Option<(Option<&DavResource>, &'x str)>; + fn map_parent<'x>(&self, resource: &'x str) -> Option<(Option>, &'x str)>; #[allow(clippy::type_complexity)] fn map_parent_resource<'x, T: FromDavResource>( @@ -96,7 +95,7 @@ impl DavFileResource for DavResources { ) -> crate::Result> { resource .resource - .and_then(|r| self.paths.by_name(r)) + .and_then(|r| self.by_path(r)) .map(|r| UriResource { collection: resource.collection, account_id: resource.account_id, @@ -105,9 +104,9 @@ impl DavFileResource for DavResources { .ok_or(DavError::Code(StatusCode::NOT_FOUND)) } - fn map_parent<'x>(&self, resource: &'x str) -> Option<(Option<&DavResource>, &'x str)> { + fn map_parent<'x>(&self, resource: &'x str) -> Option<(Option>, &'x str)> { let (parent, child) = if let Some((parent, child)) = resource.rsplit_once('/') { - (Some(self.paths.by_name(parent)?), child) + (Some(self.by_path(parent)?), child) } else { (None, resource) }; @@ -120,7 +119,7 @@ impl DavFileResource for DavResources { resource: &OwnedUri<'x>, ) -> crate::Result, &'x str)>> { if let Some(r) = resource.resource { - if self.paths.by_name(r).is_none() { + if self.by_path(r).is_none() { self.map_parent(r) .map(|(parent, child)| UriResource { collection: resource.collection, @@ -138,16 +137,16 @@ impl DavFileResource for DavResources { } impl FromDavResource for u32 { - fn from_dav_resource(item: &DavResource) -> Self { - item.document_id + fn from_dav_resource(item: DavResourcePath) -> Self { + item.document_id() } } impl FromDavResource for FileItemId { - fn from_dav_resource(item: &DavResource) -> Self { + fn from_dav_resource(item: DavResourcePath) -> Self { FileItemId { - document_id: item.document_id, - parent_id: item.parent_id, + document_id: item.document_id(), + parent_id: item.parent_id(), is_container: item.is_container(), } } diff --git a/crates/dav/src/file/proppatch.rs b/crates/dav/src/file/proppatch.rs index bc48f05a..e3b55413 100644 --- a/crates/dav/src/file/proppatch.rs +++ b/crates/dav/src/file/proppatch.rs @@ -13,10 +13,13 @@ use dav_proto::{ response::{BaseCondition, MultiStatus, Response}, }, }; -use groupware::{file::FileNode, hierarchy::DavHierarchy}; +use groupware::{cache::GroupwareCache, file::FileNode}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; use store::write::BatchBuilder; use trc::AddContext; @@ -62,7 +65,7 @@ impl FilePropPatchRequestHandler for Server { let uri = headers.uri; let account_id = resource_.account_id; let files = self - .fetch_dav_resources(access_token, account_id, Collection::FileNode) + .fetch_dav_resources(access_token, account_id, SyncCollection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_resource(&resource_)?; diff --git a/crates/dav/src/file/update.rs b/crates/dav/src/file/update.rs index b307264c..f4aaae15 100644 --- a/crates/dav/src/file/update.rs +++ b/crates/dav/src/file/update.rs @@ -4,31 +4,33 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{ - Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder, -}; -use dav_proto::{RequestHeaders, Return, schema::property::Rfc1123DateTime}; -use groupware::{ - file::{FileNode, FileProperties}, - hierarchy::DavHierarchy, -}; -use http_proto::HttpResponse; -use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; -use store::write::{BatchBuilder, now}; -use trc::AddContext; -use utils::BlobHash; - use crate::{ DavError, DavMethod, common::{ ETag, ExtractETag, - acl::DavAclHandler, + acl::ResourceAcl, lock::{LockRequestHandler, ResourceState}, uri::DavUriResource, }, file::DavFileResource, }; +use common::{ + Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder, +}; +use dav_proto::{RequestHeaders, Return, schema::property::Rfc1123DateTime}; +use groupware::{ + cache::GroupwareCache, + file::{FileNode, FileProperties}, +}; +use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; +use store::write::{BatchBuilder, now}; +use trc::AddContext; +use utils::BlobHash; pub(crate) trait FileUpdateRequestHandler: Sync + Send { fn handle_file_update_request( @@ -54,8 +56,8 @@ impl FileUpdateRequestHandler for Server { .await? .into_owned_uri()?; let account_id = resource.account_id; - let files = self - .fetch_dav_resources(access_token, account_id, Collection::FileNode) + let resources = self + .fetch_dav_resources(access_token, account_id, SyncCollection::FileNode) .await .caused_by(trc::location!())?; let resource_name = resource @@ -66,7 +68,7 @@ impl FileUpdateRequestHandler for Server { return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE)); } - if let Some(document_id) = files.paths.by_name(resource_name).map(|r| r.document_id) { + if let Some(document_id) = resources.by_path(resource_name).map(|r| r.document_id()) { // Update let node_ = self .get_archive(account_id, Collection::FileNode, document_id) @@ -193,28 +195,20 @@ impl FileUpdateRequestHandler for Server { } else { // Insert let orig_resource_name = resource_name; - let (parent, resource_name) = files + let (parent, resource_name) = resources .map_parent(resource_name) .ok_or(DavError::Code(StatusCode::CONFLICT))?; // Validate ACL - let parent_id = self - .validate_and_map_parent_acl( - access_token, - account_id, - Collection::FileNode, - parent.map(|r| r.document_id), - Acl::AddItems, - ) - .await?; + let parent_id = resources.validate_and_map_parent_acl( + access_token, + access_token.is_member(account_id), + parent.map(|r| r.document_id()), + Acl::AddItems, + )?; // Verify that parent is a collection - if parent_id > 0 - && files - .paths - .by_id(parent_id - 1) - .is_some_and(|r| !r.is_container()) - { + if parent.as_ref().is_some_and(|r| !r.is_container()) { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index 08a92ff7..82348017 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -16,6 +16,7 @@ use dav_proto::schema::{ response::{Href, MultiStatus, PropStat, Response}, }; use directory::{QueryBy, backend::internal::manage::ManageDirectory}; +use groupware::cache::GroupwareCache; use hyper::StatusCode; use jmap_proto::types::collection::Collection; use percent_encoding::NON_ALPHANUMERIC; @@ -23,7 +24,7 @@ use trc::AddContext; use crate::{ DavResourceName, - common::{propfind::PropFindRequestHandler, uri::Urn}, + common::propfind::{PropFindRequestHandler, SyncTokenUrn}, }; use super::CurrentUserPrincipal; @@ -184,17 +185,13 @@ impl PrincipalPropFind for Server { fields.push(DavPropertyValue::new(property.clone(), quota.used)); } WebDavProperty::SyncToken if !is_principal => { - let todo = "fix"; - let id = self - .store() - .get_last_change_id(account_id, collection) + let sync_token = self + .fetch_dav_resources(access_token, account_id, collection.into()) .await .caused_by(trc::location!())? - .unwrap_or_default(); - fields.push(DavPropertyValue::new( - property.clone(), - Urn::Sync { id, seq: 0 }.to_string(), - )); + .sync_token(); + + fields.push(DavPropertyValue::new(property.clone(), sync_token)); } WebDavProperty::Owner => { fields.push(DavPropertyValue::new( diff --git a/crates/dav/src/request.rs b/crates/dav/src/request.rs index c47d0c58..ac6b6097 100644 --- a/crates/dav/src/request.rs +++ b/crates/dav/src/request.rs @@ -4,27 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::sync::Arc; - -use common::{Server, auth::AccessToken}; -use dav_proto::{ - RequestHeaders, - parser::{DavParser, tokenizer::Tokenizer}, - schema::{ - Namespace, - property::WebDavProperty, - request::{Acl, LockInfo, MkCol, PropFind, PropertyUpdate, Report}, - response::{ - BaseCondition, ErrorResponse, PrincipalSearchProperty, PrincipalSearchPropertySet, - }, - }, - xml_pretty_print, -}; -use directory::Permission; -use http_proto::{HttpRequest, HttpResponse, HttpSessionData, request::fetch_body}; -use hyper::{StatusCode, header}; -use jmap_proto::types::collection::Collection; - use crate::{ DavError, DavMethod, DavResourceName, calendar::{ @@ -53,6 +32,25 @@ use crate::{ }, principal::{matching::PrincipalMatching, propsearch::PrincipalPropSearch}, }; +use common::{Server, auth::AccessToken}; +use dav_proto::{ + RequestHeaders, + parser::{DavParser, tokenizer::Tokenizer}, + schema::{ + Namespace, + property::WebDavProperty, + request::{Acl, LockInfo, MkCol, PropFind, PropertyUpdate, Report}, + response::{ + BaseCondition, ErrorResponse, PrincipalSearchProperty, PrincipalSearchPropertySet, + }, + }, + xml_pretty_print, +}; +use directory::Permission; +use http_proto::{HttpRequest, HttpResponse, HttpSessionData, request::fetch_body}; +use hyper::{StatusCode, header}; +use jmap_proto::types::collection::Collection; +use std::sync::Arc; pub trait DavRequestHandler: Sync + Send { fn handle_dav_request( @@ -452,7 +450,7 @@ impl DavRequestHandler for Server { Vec::new() }; - let c = println!("------------------------------------------"); + //let c = println!("------------------------------------------"); let std_body = std::str::from_utf8(&body).unwrap_or("[binary]").to_string(); @@ -513,7 +511,7 @@ impl DavRequestHandler for Server { Err(DavError::Code(code)) => HttpResponse::new(code), }; - let c = println!( + /*let c = println!( "{:?} {} -> {:?}\nHeaders: {:?}\nBody: {}\nResponse headers: {:?}\nResponse: {}", method, request.uri().path(), @@ -526,7 +524,7 @@ impl DavRequestHandler for Server { http_proto::HttpResponseBody::Empty => "[empty]".to_string(), _ => "[binary]".to_string(), } - ); + );*/ result } diff --git a/crates/email/src/mailbox/index.rs b/crates/email/src/mailbox/index.rs index ce70d6d0..49f09313 100644 --- a/crates/email/src/mailbox/index.rs +++ b/crates/email/src/mailbox/index.rs @@ -4,10 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::storage::{ - folder::FolderHierarchy, - index::{IndexValue, IndexableAndSerializableObject, IndexableObject}, -}; +use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject}; use jmap_proto::types::{collection::SyncCollection, value::AclGrant}; use super::{ArchivedMailbox, Mailbox}; @@ -46,21 +43,3 @@ impl IndexableObject for &ArchivedMailbox { } impl IndexableAndSerializableObject for Mailbox {} - -impl FolderHierarchy for ArchivedMailbox { - fn name(&self) -> String { - self.name.to_string() - } - - fn parent_id(&self) -> u32 { - u32::from(self.parent_id) - } - - fn is_container(&self) -> bool { - true - } - - fn size(&self) -> u32 { - 0 - } -} diff --git a/crates/groupware/Cargo.toml b/crates/groupware/Cargo.toml index deafb2ff..ac69f255 100644 --- a/crates/groupware/Cargo.toml +++ b/crates/groupware/Cargo.toml @@ -14,6 +14,7 @@ directory = { path = "../directory" } dav-proto = { path = "../dav-proto" } calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] } hashify = "0.2" +tokio = { version = "1.23", features = ["net", "macros"] } rkyv = { version = "0.8.10", features = ["little_endian"] } percent-encoding = "2.3.1" compact_str = "0.9.0" diff --git a/crates/groupware/src/cache/calcard.rs b/crates/groupware/src/cache/calcard.rs new file mode 100644 index 00000000..ea86f62d --- /dev/null +++ b/crates/groupware/src/cache/calcard.rs @@ -0,0 +1,300 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use super::GroupwareCache; +use crate::{ + DavResourceName, + calendar::{ArchivedCalendar, ArchivedCalendarEvent, Calendar, CalendarEvent}, + contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard}, +}; +use calcard::common::timezone::Tz; +use common::{ + DavName, DavPath, DavResource, DavResourceMetadata, DavResources, Server, auth::AccessToken, +}; +use directory::backend::internal::manage::ManageDirectory; +use jmap_proto::types::{ + collection::{Collection, SyncCollection}, + value::AclGrant, +}; +use percent_encoding::NON_ALPHANUMERIC; +use std::sync::Arc; +use store::ahash::{AHashMap, AHashSet}; +use tokio::sync::Semaphore; +use trc::AddContext; +use utils::map::bitmap::Bitmap; + +pub(super) async fn build_calcard_resources( + server: &Server, + access_token: &AccessToken, + account_id: u32, + sync_collection: SyncCollection, + container_collection: Collection, + item_collection: Collection, + update_lock: Arc, +) -> trc::Result { + let mut last_change_id = server + .core + .storage + .data + .get_last_change_id(account_id, sync_collection) + .await + .caused_by(trc::location!())? + .unwrap_or_default(); + + // Create default folders + let is_calendar = matches!(sync_collection, SyncCollection::Calendar); + let mut container_ids = server + .get_document_ids(account_id, container_collection) + .await + .caused_by(trc::location!())? + .unwrap_or_default(); + if container_ids.is_empty() { + if is_calendar { + server + .create_default_calendar(access_token, account_id) + .await? + } else { + server + .create_default_addressbook(access_token, account_id) + .await? + } + last_change_id = server + .core + .storage + .data + .get_last_change_id(account_id, sync_collection) + .await + .caused_by(trc::location!())? + .unwrap_or_default(); + + container_ids = server + .get_document_ids(account_id, container_collection) + .await + .caused_by(trc::location!())? + .unwrap_or_default(); + } + let item_ids = server + .get_document_ids(account_id, item_collection) + .await + .caused_by(trc::location!())? + .unwrap_or_default(); + + let name = server + .store() + .get_principal_name(account_id) + .await + .caused_by(trc::location!())? + .unwrap_or_else(|| format!("_{account_id}")); + + let mut cache = DavResources { + base_path: format!( + "{}/{}/", + if is_calendar { + DavResourceName::Cal + } else { + DavResourceName::Card + } + .base_path(), + percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), + ), + paths: AHashSet::with_capacity((container_ids.len() + item_ids.len()) as usize), + resources: Vec::with_capacity((container_ids.len() + 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::() as u64, + update_lock, + }; + + for document_id in container_ids { + if let Some(archive) = server + .get_archive(account_id, container_collection, document_id) + .await + .caused_by(trc::location!())? + { + let resource = if is_calendar { + resource_from_calendar(archive.unarchive::()?, document_id) + } else { + resource_from_addressbook(archive.unarchive::()?, document_id) + }; + let path = DavPath { + path: resource.container_name().unwrap().to_string(), + parent_id: None, + hierarchy_seq: 1, + resource_idx: cache.resources.len(), + }; + + cache.size += (std::mem::size_of::() + + std::mem::size_of::() + + (path.path.len()) * 2) as u64; + cache.paths.insert(path); + cache.resources.push(resource); + } + } + let parent_range = cache.resources.len(); + + for document_id in item_ids { + if let Some(archive) = server + .get_archive(account_id, item_collection, document_id) + .await + .caused_by(trc::location!())? + { + let resource = if is_calendar { + resource_from_event(archive.unarchive::()?, document_id) + } else { + resource_from_card(archive.unarchive::()?, 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!("{}/{}", parent.container_name().unwrap(), name.name), + parent_id: Some(name.parent_id), + hierarchy_seq: 0, + resource_idx, + }; + + cache.size += + (std::mem::size_of::() + name.name.len() + path.path.len()) as u64; + cache.paths.insert(path); + } + } + cache.size += std::mem::size_of::() as u64; + cache.resources.push(resource); + } + } + + 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::>(); + + for (resource_idx, resource) in cache.resources.iter().enumerate() { + match &resource.data { + DavResourceMetadata::Calendar { name, .. } + | DavResourceMetadata::AddressBook { name, .. } => { + let path = DavPath { + path: name.to_string(), + parent_id: None, + hierarchy_seq: 1, + resource_idx, + }; + cache.size += + (std::mem::size_of::() + 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!("{parent_name}/{}", name.name), + parent_id: Some(name.parent_id), + hierarchy_seq: 1, + resource_idx, + }; + cache.size += (std::mem::size_of::() + + name.name.len() + + path.path.len()) as u64; + cache.paths.insert(path); + } + } + } + _ => unreachable!(), + } + cache.size += std::mem::size_of::() 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(), + tz: calendar + .preferences + .first() + .and_then(|pref| pref.time_zone.tz()) + .unwrap_or(Tz::UTC), + }, + } +} + +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_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(), + }, + } +} diff --git a/crates/groupware/src/cache/file.rs b/crates/groupware/src/cache/file.rs new file mode 100644 index 00000000..c7dd849b --- /dev/null +++ b/crates/groupware/src/cache/file.rs @@ -0,0 +1,182 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + DavResourceName, + file::{ArchivedFileNode, FileNode}, +}; +use common::{DavPath, DavResource, DavResourceMetadata, DavResources, Server}; +use directory::backend::internal::manage::ManageDirectory; +use jmap_proto::types::{ + collection::{Collection, SyncCollection}, + property::Property, + value::AclGrant, +}; +use percent_encoding::NON_ALPHANUMERIC; +use std::sync::Arc; +use store::{ + Deserialize, IterateParams, U32_LEN, ValueKey, + ahash::{AHashMap, AHashSet}, + write::{AlignedBytes, Archive, ValueClass, key::DeserializeBigEndian}, +}; +use tokio::sync::Semaphore; +use trc::AddContext; +use utils::{map::bitmap::Bitmap, topological::TopologicalSort}; + +pub(super) async fn build_file_resources( + server: &Server, + account_id: u32, + update_lock: Arc, +) -> trc::Result { + let last_change_id = server + .core + .storage + .data + .get_last_change_id(account_id, SyncCollection::FileNode) + .await + .caused_by(trc::location!())? + .unwrap_or_default(); + let name = server + .store() + .get_principal_name(account_id) + .await + .caused_by(trc::location!())? + .unwrap_or_else(|| format!("_{account_id}")); + let resources = fetch_files(server, account_id).await?; + let mut files = DavResources { + base_path: format!( + "{}/{}/", + DavResourceName::File.base_path(), + percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), + ), + size: std::mem::size_of::() 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: resource.container_name().unwrap().to_string(), + 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; + if let Some((name, parent_name)) = 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(|folder| (name, &folder.path)) + }) + { + let name = format!("{parent_name}/{name}"); + let folder = names.get_mut(&folder_id).unwrap(); + folder.path = name; + folder.hierarchy_seq = hierarchy_sequence as u32; + } else { + names.get_mut(&folder_id).unwrap().hierarchy_seq = hierarchy_sequence as u32; + } + } + } + + resources.paths = names + .into_values() + .inspect(|v| { + resources.size += (std::mem::size_of::() + + std::mem::size_of::() + + std::mem::size_of::() + + std::mem::size_of::() + + v.path.len()) as u64; + }) + .collect(); +} + +async fn fetch_files(server: &Server, account_id: u32) -> trc::Result> { + let mut files = Vec::with_capacity(16); + + server + .store() + .iterate( + IterateParams::new( + ValueKey { + account_id, + collection: Collection::FileNode.into(), + document_id: 0, + class: ValueClass::Property(Property::Value.into()), + }, + ValueKey { + account_id, + collection: Collection::FileNode.into(), + document_id: u32::MAX, + class: ValueClass::Property(Property::Value.into()), + }, + ), + |key, value| { + let archive = as Deserialize>::deserialize(value)?; + + files.push(resource_from_file( + archive.unarchive::()?, + key.deserialize_be_u32(key.len() - U32_LEN)?, + )); + + Ok(true) + }, + ) + .await + .caused_by(trc::location!())?; + + Ok(files) +} + +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(), + }, + } +} diff --git a/crates/groupware/src/cache/mod.rs b/crates/groupware/src/cache/mod.rs new file mode 100644 index 00000000..46970101 --- /dev/null +++ b/crates/groupware/src/cache/mod.rs @@ -0,0 +1,414 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + calendar::{Calendar, CalendarEvent, CalendarPreferences}, + contact::{AddressBook, ContactCard}, + file::FileNode, +}; +use calcard::{ + build_calcard_resources, build_simple_hierarchy, resource_from_addressbook, + resource_from_calendar, resource_from_card, resource_from_event, +}; +use common::{CacheSwap, DavResource, DavResources, Server, auth::AccessToken}; +use file::{build_file_resources, build_nested_hierarchy, resource_from_file}; +use jmap_proto::types::collection::{Collection, SyncCollection}; +use std::sync::Arc; +use store::{ + ahash::AHashMap, + query::log::{Change, Query}, + write::{AlignedBytes, Archive, BatchBuilder}, +}; +use tokio::sync::Semaphore; +use trc::AddContext; + +pub mod calcard; +pub mod file; + +pub trait GroupwareCache: Sync + Send { + fn fetch_dav_resources( + &self, + access_token: &AccessToken, + account_id: u32, + collection: SyncCollection, + ) -> impl Future>> + Send; + + fn create_default_addressbook( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> impl Future> + Send; + + fn create_default_calendar( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> impl Future> + Send; + + fn cached_dav_resources( + &self, + account_id: u32, + collection: SyncCollection, + ) -> Option>; +} + +impl GroupwareCache for Server { + async fn fetch_dav_resources( + &self, + access_token: &AccessToken, + account_id: u32, + collection: SyncCollection, + ) -> trc::Result> { + let cache_store = match collection { + SyncCollection::Calendar => &self.inner.cache.events, + SyncCollection::AddressBook => &self.inner.cache.contacts, + SyncCollection::FileNode => &self.inner.cache.files, + _ => unreachable!(), + }; + let cache_ = match cache_store.get_value_or_guard_async(&account_id).await { + Ok(cache) => cache, + Err(guard) => { + let cache = full_cache_build( + self, + account_id, + collection, + Arc::new(Semaphore::new(1)), + access_token, + ) + .await?; + if guard.insert(CacheSwap::new(cache.clone())).is_err() { + cache_store.insert(account_id, CacheSwap::new(cache.clone())); + } + return Ok(cache); + } + }; + + // Perform full refresh on stale ids + let cache = cache_.load_full(); + if cache.highest_change_id > 0 + && self + .core + .jmap + .changes_max_history + .and_then(|history| self.inner.data.jmap_id_gen.past_id(history)) + .is_some_and(|last_change_id| cache.highest_change_id < last_change_id) + { + let cache = full_cache_build( + self, + account_id, + collection, + cache.update_lock.clone(), + access_token, + ) + .await?; + cache_.update(cache.clone()); + return Ok(cache); + } + + // Obtain current state + let changes = self + .core + .storage + .data + .changes( + account_id, + collection, + Query::Since(cache.highest_change_id), + ) + .await + .caused_by(trc::location!())?; + + // Verify changes + if changes.changes.is_empty() { + return Ok(cache); + } + + // Lock for updates + let _permit = cache.update_lock.acquire().await; + let cache = cache_.load_full(); + if cache.highest_change_id >= changes.to_change_id { + return Ok(cache); + } + + let mut updated_resources = AHashMap::with_capacity(8); + let has_no_children = collection == SyncCollection::FileNode; + + for change in changes.changes { + match change { + Change::InsertItem(id) | Change::UpdateItem(id) => { + let document_id = id as u32; + if let Some(archive) = self + .get_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) = self + .get_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(_) => (), + } + } + + 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; + } + + let cache = 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::() as u64, + update_lock: cache.update_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: cache.update_lock.clone(), + } + }; + + let cache = Arc::new(cache); + cache_.update(cache.clone()); + + Ok(cache) + } + + async fn create_default_addressbook( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> trc::Result<()> { + if let Some(name) = &self.core.groupware.default_addressbook_name { + let mut batch = BatchBuilder::new(); + let document_id = self + .store() + .assign_document_ids(account_id, Collection::AddressBook, 1) + .await?; + AddressBook { + name: name.clone(), + display_name: self.core.groupware.default_addressbook_display_name.clone(), + is_default: true, + ..Default::default() + } + .insert(access_token, account_id, document_id, &mut batch)?; + self.commit_batch(batch).await?; + } + + Ok(()) + } + + async fn create_default_calendar( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> trc::Result<()> { + if let Some(name) = &self.core.groupware.default_calendar_name { + let mut batch = BatchBuilder::new(); + let document_id = self + .store() + .assign_document_ids(account_id, Collection::Calendar, 1) + .await?; + Calendar { + name: name.clone(), + preferences: vec![CalendarPreferences { + account_id, + name: name.clone(), + description: self.core.groupware.default_calendar_display_name.clone(), + ..Default::default() + }], + ..Default::default() + } + .insert(access_token, account_id, document_id, &mut batch)?; + self.commit_batch(batch).await?; + } + + Ok(()) + } + + fn cached_dav_resources( + &self, + account_id: u32, + collection: SyncCollection, + ) -> Option> { + (match collection { + SyncCollection::Calendar => &self.inner.cache.events, + SyncCollection::AddressBook => &self.inner.cache.contacts, + SyncCollection::FileNode => &self.inner.cache.files, + _ => unreachable!(), + }) + .get(&account_id) + .map(|cache| cache.load_full()) + } +} + +async fn full_cache_build( + server: &Server, + account_id: u32, + collection: SyncCollection, + update_lock: Arc, + access_token: &AccessToken, +) -> trc::Result> { + match collection { + SyncCollection::Calendar => { + build_calcard_resources( + server, + access_token, + account_id, + SyncCollection::Calendar, + Collection::Calendar, + Collection::CalendarEvent, + update_lock, + ) + .await + } + SyncCollection::AddressBook => { + build_calcard_resources( + server, + access_token, + account_id, + SyncCollection::AddressBook, + Collection::AddressBook, + Collection::ContactCard, + update_lock, + ) + .await + } + SyncCollection::FileNode => build_file_resources(server, account_id, update_lock).await, + _ => unreachable!(), + } + .map(Arc::new) +} + +fn resource_from_archive( + archive: Archive, + document_id: u32, + collection: SyncCollection, + is_container: bool, +) -> trc::Result { + Ok(match collection { + SyncCollection::Calendar => { + if is_container { + resource_from_calendar( + archive + .unarchive::() + .caused_by(trc::location!())?, + document_id, + ) + } else { + resource_from_event( + archive + .unarchive::() + .caused_by(trc::location!())?, + document_id, + ) + } + } + SyncCollection::AddressBook => { + if is_container { + resource_from_addressbook( + archive + .unarchive::() + .caused_by(trc::location!())?, + document_id, + ) + } else { + resource_from_card( + archive + .unarchive::() + .caused_by(trc::location!())?, + document_id, + ) + } + } + SyncCollection::FileNode => resource_from_file( + archive + .unarchive::() + .caused_by(trc::location!())?, + document_id, + ), + _ => unreachable!(), + }) +} diff --git a/crates/groupware/src/calendar/dates.rs b/crates/groupware/src/calendar/dates.rs index 0e4caef5..f7947cdd 100644 --- a/crates/groupware/src/calendar/dates.rs +++ b/crates/groupware/src/calendar/dates.rs @@ -19,7 +19,6 @@ use calcard::{ }; use chrono::{DateTime, TimeZone}; use dav_proto::schema::property::TimeRange; -use rkyv::time; use std::str::FromStr; use store::{ ahash::AHashMap, diff --git a/crates/groupware/src/calendar/index.rs b/crates/groupware/src/calendar/index.rs index f2e11d11..f0e30b72 100644 --- a/crates/groupware/src/calendar/index.rs +++ b/crates/groupware/src/calendar/index.rs @@ -4,37 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::storage::index::{ - IndexItem, IndexValue, IndexableAndSerializableObject, IndexableObject, -}; -use jmap_proto::types::{collection::SyncCollection, value::AclGrant}; -use store::{SerializeInfallible, write::key::KeySerializer}; - -use crate::{IDX_NAME, IDX_TIME, IDX_UID}; - use super::{ ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarPreferences, ArchivedDefaultAlert, ArchivedTimezone, Calendar, CalendarEvent, CalendarPreferences, DefaultAlert, Timezone, }; +use crate::IDX_UID; +use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject}; +use jmap_proto::types::{collection::SyncCollection, value::AclGrant}; impl IndexableObject for Calendar { fn index_values(&self) -> impl Iterator> { - // Note: When adding a new value with index id above 0u8, tune `build_hierarchy`` to skip - // this value during iteration. [ - IndexValue::Index { - field: IDX_NAME, - value: self.name.as_str().into(), - }, - IndexValue::Index { - field: IDX_TIME, - value: self - .preferences - .first() - .and_then(|p| p.time_zone.tz()) - .map(|tz| tz.as_id().serialize()) - .into(), - }, IndexValue::Acl { value: (&self.acls).into(), }, @@ -55,19 +35,6 @@ impl IndexableObject for Calendar { impl IndexableObject for &ArchivedCalendar { fn index_values(&self) -> impl Iterator> { [ - IndexValue::Index { - field: IDX_NAME, - value: self.name.as_str().into(), - }, - IndexValue::Index { - field: IDX_TIME, - value: self - .preferences - .first() - .and_then(|p| p.time_zone.tz()) - .map(|tz| tz.as_id().serialize()) - .into(), - }, IndexValue::Acl { value: self .acls @@ -95,31 +62,10 @@ impl IndexableAndSerializableObject for Calendar {} impl IndexableObject for CalendarEvent { fn index_values(&self) -> impl Iterator> { [ - IndexValue::IndexList { - field: IDX_NAME, - value: self - .names - .iter() - .map(|v| IndexItem::Vec(v.serialize())) - .collect::>(), - }, IndexValue::Index { field: IDX_UID, value: self.data.event.uids().next().into(), }, - IndexValue::Index { - field: IDX_TIME, - value: self - .data - .event_range() - .map(|(start, duration)| { - KeySerializer::new(std::mem::size_of::() + std::mem::size_of::()) - .write(start as u64) - .write(duration) - .finalize() - }) - .into(), - }, IndexValue::Quota { used: self.dead_properties.size() as u32 + self.display_name.as_ref().map_or(0, |n| n.len() as u32) @@ -138,31 +84,10 @@ impl IndexableObject for CalendarEvent { impl IndexableObject for &ArchivedCalendarEvent { fn index_values(&self) -> impl Iterator> { [ - IndexValue::IndexList { - field: IDX_NAME, - value: self - .names - .iter() - .map(|v| IndexItem::Vec(v.serialize())) - .collect::>(), - }, IndexValue::Index { field: IDX_UID, value: self.data.event.uids().next().into(), }, - IndexValue::Index { - field: IDX_TIME, - value: self - .data - .event_range() - .map(|(start, duration)| { - KeySerializer::new(std::mem::size_of::() + std::mem::size_of::()) - .write(start as u64) - .write(duration) - .finalize() - }) - .into(), - }, IndexValue::Quota { used: self.dead_properties.size() as u32 + self.display_name.as_ref().map_or(0, |n| n.len() as u32) diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index f458a5e5..a3c4f110 100644 --- a/crates/groupware/src/calendar/mod.rs +++ b/crates/groupware/src/calendar/mod.rs @@ -8,8 +8,8 @@ pub mod dates; pub mod index; pub mod storage; -use crate::DavName; use calcard::icalendar::ICalendar; +use common::DavName; use dav_proto::schema::request::DeadProperty; use jmap_proto::types::{acl::Acl, value::AclGrant}; use store::{SERIALIZE_CALENDAR_V1, SERIALIZE_CALENDAREVENT_V1, SerializedVersion}; diff --git a/crates/groupware/src/contact/index.rs b/crates/groupware/src/contact/index.rs index 1de1e962..aef7ca27 100644 --- a/crates/groupware/src/contact/index.rs +++ b/crates/groupware/src/contact/index.rs @@ -4,25 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::storage::index::{ - IndexItem, IndexValue, IndexableAndSerializableObject, IndexableObject, -}; -use jmap_proto::types::{collection::SyncCollection, value::AclGrant}; -use store::SerializeInfallible; - -use crate::{IDX_NAME, IDX_UID}; - use super::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard}; +use crate::IDX_UID; +use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject}; +use jmap_proto::types::{collection::SyncCollection, value::AclGrant}; impl IndexableObject for AddressBook { fn index_values(&self) -> impl Iterator> { - // Note: When adding a new value with index id above 0u8, tune `build_hierarchy`` to skip - // this value during iteration. [ - IndexValue::Index { - field: IDX_NAME, - value: self.name.as_str().into(), - }, IndexValue::Acl { value: (&self.acls).into(), }, @@ -43,10 +32,6 @@ impl IndexableObject for AddressBook { impl IndexableObject for &ArchivedAddressBook { fn index_values(&self) -> impl Iterator> { [ - IndexValue::Index { - field: IDX_NAME, - value: self.name.as_str().into(), - }, IndexValue::Acl { value: self .acls @@ -74,14 +59,6 @@ impl IndexableAndSerializableObject for AddressBook {} impl IndexableObject for ContactCard { fn index_values(&self) -> impl Iterator> { [ - IndexValue::IndexList { - field: IDX_NAME, - value: self - .names - .iter() - .map(|v| IndexItem::Vec(v.serialize())) - .collect::>(), - }, IndexValue::Index { field: IDX_UID, value: self.card.uid().into(), @@ -104,14 +81,6 @@ impl IndexableObject for ContactCard { impl IndexableObject for &ArchivedContactCard { fn index_values(&self) -> impl Iterator> { [ - IndexValue::IndexList { - field: IDX_NAME, - value: self - .names - .iter() - .map(|v| IndexItem::Vec(v.serialize())) - .collect::>(), - }, IndexValue::Index { field: IDX_UID, value: self.card.uid().into(), diff --git a/crates/groupware/src/contact/mod.rs b/crates/groupware/src/contact/mod.rs index 166f8ef1..50f0aef0 100644 --- a/crates/groupware/src/contact/mod.rs +++ b/crates/groupware/src/contact/mod.rs @@ -8,13 +8,11 @@ pub mod index; pub mod storage; use calcard::vcard::VCard; - +use common::DavName; use dav_proto::schema::request::DeadProperty; use jmap_proto::types::{acl::Acl, value::AclGrant}; use store::{SERIALIZE_ADDRESSBOOK_V1, SERIALIZE_CALENDAREVENT_V1, SerializedVersion}; -use crate::DavName; - #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] diff --git a/crates/groupware/src/file/index.rs b/crates/groupware/src/file/index.rs index 59d2c831..cc43179a 100644 --- a/crates/groupware/src/file/index.rs +++ b/crates/groupware/src/file/index.rs @@ -4,57 +4,30 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::storage::{ - folder::FolderHierarchy, - index::{IndexValue, IndexableAndSerializableObject, IndexableObject}, -}; -use jmap_proto::types::{collection::SyncCollection, property::Property, value::AclGrant}; +use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject}; +use jmap_proto::types::{collection::SyncCollection, value::AclGrant}; use super::{ArchivedFileNode, FileNode}; impl IndexableObject for FileNode { fn index_values(&self) -> impl Iterator> { - let size = self.dead_properties.size() as u32 - + self.display_name.as_ref().map_or(0, |n| n.len() as u32) - + self.name.len() as u32; - let mut values = Vec::with_capacity(6); values.extend([ - IndexValue::Index { - field: Property::Name.into(), - value: percent_encoding::percent_decode_str(&self.name) - .decode_utf8() - .unwrap_or_else(|_| self.name.as_str().into()) - .to_lowercase() - .into(), - }, - IndexValue::Index { - field: Property::ParentId.into(), - value: self.parent_id.into(), - }, IndexValue::Acl { value: (&self.acls).into(), }, - IndexValue::LogContainer { + IndexValue::LogItem { + prefix: None, sync_collection: SyncCollection::FileNode.into(), }, + IndexValue::Quota { used: self.size() }, ]); if let Some(file) = &self.file { - let size = size + file.size; - values.extend([ - IndexValue::Blob { - value: file.blob_hash.clone(), - }, - IndexValue::Index { - field: Property::Size.into(), - value: size.into(), - }, - IndexValue::Quota { used: size }, - ]); - } else { - values.push(IndexValue::Quota { used: size }); + values.extend([IndexValue::Blob { + value: file.blob_hash.clone(), + }]); } values.into_iter() @@ -66,14 +39,6 @@ impl IndexableObject for &ArchivedFileNode { let mut values = Vec::with_capacity(6); values.extend([ - IndexValue::Index { - field: Property::Name.into(), - value: self.name.to_lowercase().into(), - }, - IndexValue::Index { - field: Property::ParentId.into(), - value: u32::from(self.parent_id).into(), - }, IndexValue::Acl { value: self .acls @@ -82,25 +47,17 @@ impl IndexableObject for &ArchivedFileNode { .collect::>() .into(), }, - IndexValue::LogContainer { + IndexValue::LogItem { + prefix: None, sync_collection: SyncCollection::FileNode.into(), }, + IndexValue::Quota { used: self.size() }, ]); - let size = self.size(); if let Some(file) = self.file.as_ref() { - values.extend([ - IndexValue::Blob { - value: (&file.blob_hash).into(), - }, - IndexValue::Index { - field: Property::Size.into(), - value: size.into(), - }, - IndexValue::Quota { used: size }, - ]); - } else { - values.push(IndexValue::Quota { used: size }); + values.extend([IndexValue::Blob { + value: (&file.blob_hash).into(), + }]); } values.into_iter() @@ -109,19 +66,11 @@ impl IndexableObject for &ArchivedFileNode { impl IndexableAndSerializableObject for FileNode {} -impl FolderHierarchy for ArchivedFileNode { - fn name(&self) -> String { - self.name.to_string() - } - - fn parent_id(&self) -> u32 { - u32::from(self.parent_id) - } - - fn is_container(&self) -> bool { - self.file.is_none() - } +pub trait NodeSize { + fn size(&self) -> u32; +} +impl NodeSize for ArchivedFileNode { fn size(&self) -> u32 { self.dead_properties.size() as u32 + self.display_name.as_ref().map_or(0, |n| n.len() as u32) @@ -129,3 +78,12 @@ impl FolderHierarchy for ArchivedFileNode { + self.file.as_ref().map_or(0, |f| u32::from(f.size)) } } + +impl NodeSize for FileNode { + fn size(&self) -> u32 { + self.dead_properties.size() as u32 + + self.display_name.as_ref().map_or(0, |n| n.len() as u32) + + self.name.len() as u32 + + self.file.as_ref().map_or(0, |f| f.size) + } +} diff --git a/crates/groupware/src/hierarchy.rs b/crates/groupware/src/hierarchy.rs deleted file mode 100644 index b514a7cc..00000000 --- a/crates/groupware/src/hierarchy.rs +++ /dev/null @@ -1,372 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::{ - DavName, DavResourceName, IDX_NAME, IDX_TIME, - calendar::{Calendar, CalendarPreferences}, - contact::AddressBook, - file::FileNode, -}; -use calcard::common::timezone::Tz; -use common::{ - DavResource, DavResourceId, DavResourceMetadata, DavResources, Server, auth::AccessToken, -}; -use directory::backend::internal::manage::ManageDirectory; -use jmap_proto::types::collection::Collection; -use percent_encoding::NON_ALPHANUMERIC; -use std::sync::Arc; -use store::{ - Deserialize, IndexKey, IndexKeyPrefix, IterateParams, SerializeInfallible, U32_LEN, U64_LEN, - ahash::AHashMap, - write::{BatchBuilder, key::DeserializeBigEndian}, -}; -use trc::AddContext; -use utils::bimap::IdBimap; - -pub trait DavHierarchy: Sync + Send { - fn fetch_dav_resources( - &self, - access_token: &AccessToken, - account_id: u32, - collection: Collection, - ) -> impl Future>> + Send; - - fn create_default_addressbook( - &self, - access_token: &AccessToken, - account_id: u32, - ) -> impl Future> + Send; - - fn create_default_calendar( - &self, - access_token: &AccessToken, - account_id: u32, - ) -> impl Future> + Send; - - fn cached_dav_resources( - &self, - account_id: u32, - collection: Collection, - ) -> Option>; -} - -impl DavHierarchy for Server { - async fn fetch_dav_resources( - &self, - access_token: &AccessToken, - account_id: u32, - collection: Collection, - ) -> trc::Result> { - let todo = "fix"; - - let is_files = collection == Collection::FileNode; - let mut change_id = self - .store() - .get_last_change_id(account_id, collection) - .await - .caused_by(trc::location!())?; - if !is_files { - let child_change_id = self - .store() - .get_last_change_id(account_id, collection.child_collection().unwrap()) - .await - .caused_by(trc::location!())?; - change_id = change_id.max(child_change_id); - } - - let resource_id = DavResourceId { - account_id, - collection: collection.into(), - }; - if let Some(files) = self - .inner - .cache - .dav - .get(&resource_id) - .filter(|x| x.modseq == change_id) - { - Ok(files) - } else { - let mut files = if !is_files { - let files = build_hierarchy(self, account_id, collection).await?; - if files.paths.is_empty() { - match collection { - Collection::Calendar => { - self.create_default_calendar(access_token, account_id) - .await? - } - Collection::AddressBook => { - self.create_default_addressbook(access_token, account_id) - .await? - } - _ => unreachable!(), - } - build_hierarchy(self, account_id, collection).await? - } else { - files - } - } else { - build_file_hierarchy(self, account_id).await? - }; - - files.modseq = change_id; - let files = Arc::new(files); - self.inner.cache.dav.insert(resource_id, files.clone()); - - Ok(files) - } - } - - async fn create_default_addressbook( - &self, - access_token: &AccessToken, - account_id: u32, - ) -> trc::Result<()> { - if let Some(name) = &self.core.groupware.default_addressbook_name { - let mut batch = BatchBuilder::new(); - let document_id = self - .store() - .assign_document_ids(account_id, Collection::AddressBook, 1) - .await?; - AddressBook { - name: name.clone(), - display_name: self.core.groupware.default_addressbook_display_name.clone(), - is_default: true, - ..Default::default() - } - .insert(access_token, account_id, document_id, &mut batch)?; - self.commit_batch(batch).await?; - } - - Ok(()) - } - - async fn create_default_calendar( - &self, - access_token: &AccessToken, - account_id: u32, - ) -> trc::Result<()> { - if let Some(name) = &self.core.groupware.default_calendar_name { - let mut batch = BatchBuilder::new(); - let document_id = self - .store() - .assign_document_ids(account_id, Collection::Calendar, 1) - .await?; - Calendar { - name: name.clone(), - preferences: vec![CalendarPreferences { - account_id, - name: name.clone(), - description: self.core.groupware.default_calendar_display_name.clone(), - ..Default::default() - }], - ..Default::default() - } - .insert(access_token, account_id, document_id, &mut batch)?; - self.commit_batch(batch).await?; - } - - Ok(()) - } - - fn cached_dav_resources( - &self, - account_id: u32, - collection: Collection, - ) -> Option> { - self.inner - .cache - .dav - .get(&DavResourceId { - account_id, - collection: collection.into(), - }) - .clone() - } -} - -async fn build_hierarchy( - server: &Server, - account_id: u32, - collection_: Collection, -) -> trc::Result { - let base_path = DavResourceName::from(collection_).base_path(); - let collection = u8::from(collection_); - let mut containers: AHashMap = AHashMap::with_capacity(16); - let mut resources: AHashMap> = AHashMap::with_capacity(16); - - let mut time_ranges: AHashMap = AHashMap::new(); - let mut time_zones: AHashMap = AHashMap::new(); - - server - .store() - .iterate( - IterateParams::new( - IndexKey { - account_id, - collection, - document_id: 0, - field: IDX_NAME, - key: 0u32.serialize(), - }, - IndexKey { - account_id, - collection: collection + 1, - document_id: u32::MAX, - field: IDX_TIME, - key: u32::MAX.serialize(), - }, - ) - .no_values() - .ascending(), - |key, _| { - let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?; - let value = key - .get(IndexKeyPrefix::len()..key.len() - U32_LEN) - .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?; - let key_collection = key - .get(U32_LEN) - .copied() - .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?; - let field = key - .get(U32_LEN + 1) - .copied() - .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?; - - if key_collection == collection { - if field == IDX_NAME { - containers.insert( - document_id, - std::str::from_utf8(value) - .map_err(|_| { - trc::Error::corrupted_key(key, None, trc::location!()) - })? - .to_string(), - ); - } else if field == IDX_TIME { - let tz = Tz::from_id(key.deserialize_be_u16(IndexKeyPrefix::len())?) - .ok_or_else(|| { - trc::Error::corrupted_key(key, None, trc::location!()) - })?; - - time_zones.insert(document_id, tz); - } - } else if field == IDX_NAME { - resources.entry(document_id).or_default().push( - DavName::deserialize(value) - .map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))?, - ); - } else if field == IDX_TIME { - let start_time = key.deserialize_be_u64(IndexKeyPrefix::len())?; - let duration = key.deserialize_be_u32(IndexKeyPrefix::len() + U64_LEN)?; - - time_ranges.insert(document_id, (start_time as i64, duration)); - } - - Ok(true) - }, - ) - .await - .caused_by(trc::location!())?; - - let name = server - .store() - .get_principal_name(account_id) - .await - .caused_by(trc::location!())? - .unwrap_or_else(|| format!("_{account_id}")); - - let mut files = DavResources { - paths: IdBimap::with_capacity(containers.len() + resources.len()), - size: std::mem::size_of::() as u64, - modseq: None, - base_path: format!( - "{}/{}/", - base_path, - percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), - ), - }; - - for (document_id, dav_names) in resources { - for dav_name in dav_names { - if let Some(container) = containers.get(&dav_name.parent_id) { - let name = format!("{}/{}", container, dav_name.name); - files.size += (std::mem::size_of::() - + std::mem::size_of::() - + name.len()) as u64; - files.paths.insert(DavResource { - document_id, - parent_id: dav_name.parent_id.into(), - name, - data: time_ranges - .get(&document_id) - .map(|(start, duration)| DavResourceMetadata::CalendarEvent { - start: *start, - duration: *duration, - }) - .unwrap_or(DavResourceMetadata::None), - }); - } - } - } - - for (document_id, name) in containers { - files.size += - (std::mem::size_of::() + std::mem::size_of::() + name.len()) as u64; - files.paths.insert(DavResource { - document_id, - parent_id: None, - name, - data: time_zones - .get(&document_id) - .map(|tz| DavResourceMetadata::Calendar { tz: *tz }) - .unwrap_or(DavResourceMetadata::None), - }); - } - - Ok(files) -} - -async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result { - let list = server - .fetch_folders::(account_id, Collection::FileNode) - .await - .caused_by(trc::location!())?; - let name = server - .store() - .get_principal_name(account_id) - .await - .caused_by(trc::location!())? - .unwrap_or_else(|| format!("_{account_id}")); - let mut files = DavResources { - base_path: format!( - "{}/{}/", - DavResourceName::File.base_path(), - percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), - ), - paths: IdBimap::with_capacity(list.len()), - size: std::mem::size_of::() as u64, - modseq: None, - }; - - for expanded in list.into_iterator() { - files.size += (std::mem::size_of::() - + std::mem::size_of::() - + expanded.name.len()) as u64; - files.paths.insert(DavResource { - document_id: expanded.document_id, - parent_id: expanded.parent_id, - name: expanded.name, - data: DavResourceMetadata::File { - size: expanded.size, - hierarchy_sequence: expanded.hierarchy_sequence, - is_container: expanded.is_container, - }, - }); - } - - Ok(files) -} diff --git a/crates/groupware/src/lib.rs b/crates/groupware/src/lib.rs index a30260b5..36d784f6 100644 --- a/crates/groupware/src/lib.rs +++ b/crates/groupware/src/lib.rs @@ -7,17 +7,13 @@ use calcard::common::timezone::Tz; use common::DavResources; use jmap_proto::types::collection::Collection; -use store::{Deserialize, SerializeInfallible, write::key::KeySerializer}; -use utils::codec::leb128::Leb128Reader; +pub mod cache; pub mod calendar; pub mod contact; pub mod file; -pub mod hierarchy; -pub const IDX_NAME: u8 = 0; -pub const IDX_TIME: u8 = 1; -pub const IDX_UID: u8 = 2; +pub const IDX_UID: u8 = 0; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DavResourceName { @@ -29,61 +25,6 @@ pub enum DavResourceName { pub struct DestroyArchive(pub T); -#[derive( - rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, -)] -#[rkyv(derive(Debug))] -pub struct DavName { - pub name: String, - pub parent_id: u32, -} - -impl SerializeInfallible for DavName { - fn serialize(&self) -> Vec { - KeySerializer::new(self.name.len() + std::mem::size_of::()) - .write_leb128(self.parent_id) - .write(self.name.as_bytes()) - .finalize() - } -} - -impl SerializeInfallible for ArchivedDavName { - fn serialize(&self) -> Vec { - KeySerializer::new(self.name.len() + std::mem::size_of::()) - .write_leb128(self.parent_id.to_native()) - .write(self.name.as_bytes()) - .finalize() - } -} - -impl DavName { - pub fn new(name: String, parent_id: u32) -> Self { - Self { name, parent_id } - } -} - -impl Deserialize for DavName { - fn deserialize(bytes: &[u8]) -> trc::Result { - let (parent_id, bytes_read) = bytes.read_leb128::().ok_or_else(|| { - trc::StoreEvent::DataCorruption - .caused_by(trc::location!()) - .ctx(trc::Key::Value, bytes) - })?; - - let name = bytes - .get(bytes_read..) - .and_then(|bytes| std::str::from_utf8(bytes).ok()) - .ok_or_else(|| { - trc::StoreEvent::DataCorruption - .caused_by(trc::location!()) - .ctx(trc::Key::Value, bytes) - })? - .into(); - - Ok(DavName { name, parent_id }) - } -} - impl DavResourceName { pub fn parse(service: &str) -> Option { hashify::tiny_map!(service.as_bytes(), @@ -138,22 +79,11 @@ impl From for DavResourceName { pub trait DavCalendarResource { fn calendar_default_tz(&self, calendar_id: u32) -> Option; - fn event_default_tz(&self, event_id: u32) -> Option; } impl DavCalendarResource for DavResources { fn calendar_default_tz(&self, calendar_id: u32) -> Option { - self.paths - .iter() - .find(|c| c.is_container() && c.document_id == calendar_id) + self.container_resource_by_id(calendar_id) .and_then(|c| c.timezone()) } - - fn event_default_tz(&self, event_id: u32) -> Option { - self.paths - .iter() - .find(|c| !c.is_container() && c.document_id == event_id) - .and_then(|c| c.parent_id) - .and_then(|parent_id| self.calendar_default_tz(parent_id)) - } } diff --git a/crates/jmap-proto/src/types/collection.rs b/crates/jmap-proto/src/types/collection.rs index d796b05e..34f4a392 100644 --- a/crates/jmap-proto/src/types/collection.rs +++ b/crates/jmap-proto/src/types/collection.rs @@ -90,6 +90,61 @@ impl Collection { } } +impl SyncCollection { + pub fn collection(&self, is_container: bool) -> Collection { + match self { + SyncCollection::Email => { + if is_container { + Collection::Mailbox + } else { + Collection::Email + } + } + SyncCollection::Thread => Collection::Thread, + SyncCollection::Calendar => { + if is_container { + Collection::Calendar + } else { + Collection::CalendarEvent + } + } + SyncCollection::AddressBook => { + if is_container { + Collection::AddressBook + } else { + Collection::ContactCard + } + } + SyncCollection::FileNode => Collection::FileNode, + SyncCollection::Identity => Collection::Identity, + SyncCollection::EmailSubmission => Collection::EmailSubmission, + SyncCollection::SieveScript => Collection::SieveScript, + SyncCollection::None => Collection::None, + } + } +} + +impl From for SyncCollection { + fn from(v: Collection) -> Self { + match v { + Collection::Email => SyncCollection::Email, + Collection::Mailbox => SyncCollection::Email, + Collection::Thread => SyncCollection::Thread, + Collection::Identity => SyncCollection::Identity, + Collection::EmailSubmission => SyncCollection::EmailSubmission, + Collection::SieveScript => SyncCollection::SieveScript, + Collection::PushSubscription => SyncCollection::None, + Collection::Principal => SyncCollection::None, + Collection::Calendar => SyncCollection::Calendar, + Collection::CalendarEvent => SyncCollection::Calendar, + Collection::AddressBook => SyncCollection::AddressBook, + Collection::ContactCard => SyncCollection::AddressBook, + Collection::FileNode => SyncCollection::FileNode, + _ => SyncCollection::None, + } + } +} + impl From for Collection { fn from(v: u8) -> Self { match v { diff --git a/tests/src/webdav/basic.rs b/tests/src/webdav/basic.rs index 5884226b..211dd837 100644 --- a/tests/src/webdav/basic.rs +++ b/tests/src/webdav/basic.rs @@ -8,11 +8,11 @@ use super::WebDavTest; pub async fn test(test: &WebDavTest) { println!("Running basic tests..."); - let client = test.client("john"); + let john = test.client("john"); + let jane = test.client("jane"); // Test OPTIONS request - client - .request("OPTIONS", "/dav/file", "") + john.request("OPTIONS", "/dav/file", "") .await .with_header( "dav", @@ -27,18 +27,21 @@ pub async fn test(test: &WebDavTest) { ); // Test Discovery - client - .request("PROPFIND", "/.well-known/carddav", "") + john.request("PROPFIND", "/.well-known/carddav", "") .await .with_values( "D:multistatus.D:response.D:href", ["/dav/card/", "/dav/card/john/"], ); - test.client("jane") - .request("PROPFIND", "/.well-known/caldav", "") + jane.request("PROPFIND", "/.well-known/caldav", "") .await .with_values( "D:multistatus.D:response.D:href", ["/dav/cal/", "/dav/cal/jane/", "/dav/cal/support/"], ); + + john.delete_default_containers().await; + jane.delete_default_containers().await; + jane.delete_default_containers_by_account("support").await; + test.assert_is_empty().await; } diff --git a/tests/src/webdav/mod.rs b/tests/src/webdav/mod.rs index c6d1f265..4270fdb3 100644 --- a/tests/src/webdav/mod.rs +++ b/tests/src/webdav/mod.rs @@ -22,7 +22,7 @@ use common::{ manager::boot::build_ipc, }; use dav_proto::schema::property::{DavProperty, WebDavProperty}; -use groupware::{DavResourceName, hierarchy::DavHierarchy}; +use groupware::{DavResourceName, cache::GroupwareCache}; use http::HttpSessionManager; use hyper::{HeaderMap, Method, StatusCode, header::AUTHORIZATION}; use imap::core::ImapSessionManager; @@ -227,17 +227,25 @@ impl WebDavTest { let account_id = self.client(name).account_id; let access_token = self.server.get_access_token(account_id).await.unwrap(); self.server - .fetch_dav_resources(&access_token, account_id, collection) + .fetch_dav_resources(&access_token, account_id, collection.into()) .await .unwrap() } pub async fn assert_is_empty(&self) { assert_is_empty(self.server.clone()).await; + for cache in [ + &self.server.inner.cache.events, + &self.server.inner.cache.contacts, + &self.server.inner.cache.files, + ] { + cache.clear(); + } } } #[allow(dead_code)] +#[derive(Debug)] pub struct DummyWebDavClient { account_id: u32, name: &'static str, @@ -653,7 +661,7 @@ pub trait DavResourcesTest { impl DavResourcesTest for DavResources { fn items(&self) -> Vec { - self.paths.iter().cloned().collect() + self.resources.clone() } } diff --git a/tests/src/webdav/principals.rs b/tests/src/webdav/principals.rs index 3a31bbdc..5cf67686 100644 --- a/tests/src/webdav/principals.rs +++ b/tests/src/webdav/principals.rs @@ -328,6 +328,7 @@ pub async fn test(test: &WebDavTest) { .with_hrefs([format!("{}/support/", DavResourceName::Principal.base_path()).as_str()]); client.delete_default_containers().await; + client.delete_default_containers_by_account("support").await; test.assert_is_empty().await; } diff --git a/tests/src/webdav/put_get.rs b/tests/src/webdav/put_get.rs index 2e110efe..5c7be61e 100644 --- a/tests/src/webdav/put_get.rs +++ b/tests/src/webdav/put_get.rs @@ -197,9 +197,15 @@ pub async fn test(test: &WebDavTest) { // PUT precondition enforcement let modseq = [ - test.resources("john", Collection::FileNode).await.modseq, - test.resources("john", Collection::Calendar).await.modseq, - test.resources("john", Collection::AddressBook).await.modseq, + test.resources("john", Collection::FileNode) + .await + .highest_change_id, + test.resources("john", Collection::Calendar) + .await + .highest_change_id, + test.resources("john", Collection::AddressBook) + .await + .highest_change_id, ]; for (path, ct, content) in [ ("/dav/file/john/file1.txt", "text/plain", TEST_FILE_1), @@ -263,9 +269,15 @@ pub async fn test(test: &WebDavTest) { } assert_eq!( [ - test.resources("john", Collection::FileNode).await.modseq, - test.resources("john", Collection::Calendar).await.modseq, - test.resources("john", Collection::AddressBook).await.modseq, + test.resources("john", Collection::FileNode) + .await + .highest_change_id, + test.resources("john", Collection::Calendar) + .await + .highest_change_id, + test.resources("john", Collection::AddressBook) + .await + .highest_change_id, ], modseq );