diff --git a/Cargo.lock b/Cargo.lock index 484c6fa2..72103b4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1680,6 +1680,7 @@ dependencies = [ "hyper 1.6.0", "jmap_proto", "percent-encoding", + "rkyv 0.8.10", "store", "trc", "utils", diff --git a/crates/common/src/config/dav.rs b/crates/common/src/config/dav.rs index 7a599a2f..4fd878b0 100644 --- a/crates/common/src/config/dav.rs +++ b/crates/common/src/config/dav.rs @@ -11,6 +11,7 @@ pub struct DavConfig { pub max_request_size: usize, pub dead_property_size: Option, pub live_property_size: usize, + pub max_lock_timeout: u64, } impl DavConfig { @@ -25,6 +26,7 @@ impl DavConfig { live_property_size: config .property("dav.limits.size.live-property") .unwrap_or(250), + max_lock_timeout: config.property("dav.limits.timeout.max-lock").unwrap_or(60), } } } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index fada65aa..6f587ad8 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -101,6 +101,7 @@ pub const KV_LOCK_QUEUE_MESSAGE: u8 = 21; pub const KV_LOCK_QUEUE_REPORT: u8 = 22; pub const KV_LOCK_EMAIL_TASK: u8 = 23; pub const KV_LOCK_HOUSEKEEPER: u8 = 24; +pub const KV_LOCK_DAV: u8 = 25; #[derive(Clone)] pub struct Server { @@ -260,6 +261,8 @@ pub struct FileItem { pub document_id: u32, pub parent_id: Option, pub name: String, + pub size: u32, + pub hierarchy_sequence: u32, pub is_container: bool, } @@ -497,6 +500,20 @@ impl Files { .filter(move |item| item.name.starts_with(&prefix) || item.name == search_path) } + pub fn subtree_with_depth( + &self, + search_path: &str, + depth: usize, + ) -> impl Iterator { + let prefix = format!("{search_path}/"); + self.files.iter().filter(move |item| { + item.name + .strip_prefix(&prefix) + .is_some_and(|name| name.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth) + || item.name == search_path + }) + } + pub fn is_ancestor_of(&self, ancestor: u32, descendant: u32) -> bool { let ancestor = &self.files.by_id(ancestor).unwrap().name; let descendant = &self.files.by_id(descendant).unwrap().name; diff --git a/crates/common/src/storage/folder.rs b/crates/common/src/storage/folder.rs index baf2e218..dd798525 100644 --- a/crates/common/src/storage/folder.rs +++ b/crates/common/src/storage/folder.rs @@ -16,7 +16,7 @@ use utils::topological::{TopologicalSort, TopologicalSortIterator}; use crate::Server; pub struct ExpandedFolders { - names: AHashMap, + names: AHashMap, iter: TopologicalSortIterator, } @@ -26,12 +26,15 @@ pub struct ExpandedFolder { 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 { @@ -75,15 +78,26 @@ impl Server { }, ), |key, value| { - let document_id = key.deserialize_be_u32(key.len() - U32_LEN)? + 1; + let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?; let archive = ::deserialize(value)?; let folder = archive.unarchive::()?; let parent_id = folder.parent_id(); - topological_sort.insert(parent_id, document_id); + topological_sort.insert(parent_id, document_id + 1); names.insert( document_id, - (folder.name(), parent_id, folder.is_container()), + 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) @@ -164,46 +178,39 @@ impl ExpandedFolders { pub fn format(mut self, formatter: T) -> Self where - T: Fn(u32, &str) -> Option, + T: Fn(&mut ExpandedFolder), { - for (document_id, (name, _, _)) in &mut self.names { - if let Some(new_name) = formatter(*document_id - 1, name) { - *name = new_name; - } + for folder in self.names.values_mut() { + formatter(folder); } self } pub fn into_iterator(mut self) -> impl Iterator + Sync + Send { - for folder_id in self.iter.by_ref() { + for (hierarchy_sequence, folder_id) in self.iter.by_ref().enumerate() { if folder_id != 0 { - if let Some((name, parent_name, parent_id, is_container)) = self + let folder_id = folder_id - 1; + if let Some((name, parent_name)) = self .names .get(&folder_id) - .and_then(|(name, parent_id, is_container)| { - self.names.get(parent_id).map(|(parent_name, _, _)| { - (name, parent_name, *parent_id, *is_container) - }) + .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}"); - self.names - .insert(folder_id, (name, parent_id, is_container)); + 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_iter() - .map(|(id, (name, parent_id, is_container))| ExpandedFolder { - name, - document_id: id - 1, - is_container, - parent_id: if parent_id == 0 { - None - } else { - Some(parent_id - 1) - }, - }) + self.names.into_values() } } diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 1f1d7cf5..7fdf574a 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -17,6 +17,7 @@ trc = { path = "../trc" } hashify = { version = "0.2" } hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } percent-encoding = "2.3.1" +rkyv = { version = "0.8.10", features = ["little_endian"] } [dev-dependencies] diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index 3726b30f..8b57b459 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -1,4 +1,10 @@ -use common::{Server, auth::AccessToken}; +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + + use common::{Server, auth::AccessToken}; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection}; use trc::AddContext; diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs new file mode 100644 index 00000000..3e5e8335 --- /dev/null +++ b/crates/dav/src/common/lock.rs @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::KV_LOCK_DAV; +use common::{Server, auth::AccessToken}; +use dav_proto::schema::property::LockScope; +use dav_proto::schema::request::DeadProperty; +use dav_proto::{Depth, Timeout}; +use dav_proto::{RequestHeaders, schema::request::LockInfo}; +use http_proto::HttpResponse; +use hyper::StatusCode; +use store::dispatch::lookup::KeyValue; +use store::write::{Archive, Archiver}; +use store::{Serialize, blake3}; +use trc::AddContext; + +use super::uri::{DavUriResource, UriResource}; +use crate::DavError; + +pub(crate) trait LockRequestHandler: Sync + Send { + fn handle_lock_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + lock_info: Option, + ) -> impl Future> + Send; +} + +impl LockRequestHandler for Server { + async fn handle_lock_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + lock_info: Option, + ) -> crate::Result { + let resource = self.validate_uri(access_token, headers.uri).await?; + let resource_hash = resource + .lock_key() + .ok_or(DavError::Code(StatusCode::CONFLICT))?; + if !access_token.is_member(resource.account_id.unwrap()) { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + + let lock_data = if let Some(lock_data) = self + .in_memory_store() + .key_get::(resource_hash.as_slice()) + .await + .caused_by(trc::location!())? + { + let lock_data = lock_data + .deserialize::() + .caused_by(trc::location!())?; + if access_token.primary_id == lock_data.owner { + Some(lock_data) + } else { + return Err(DavError::Code(StatusCode::LOCKED)); + } + } else { + None + }; + + if let Some(lock_info) = lock_info { + let timeout = if let Timeout::Second(seconds) = headers.timeout { + std::cmp::min(seconds, self.core.dav.max_lock_timeout) + } else { + self.core.dav.max_lock_timeout + }; + + let lock_data = if let Some(mut lock_data) = lock_data { + lock_data.depth_infinity = matches!(headers.depth, Depth::Infinity); + lock_data.owner_dav = lock_info.owner; + lock_data.exclusive = matches!(lock_info.lock_scope, LockScope::Exclusive); + lock_data + } else { + LockData { + owner: access_token.primary_id, + depth_infinity: matches!(headers.depth, Depth::Infinity), + owner_dav: lock_info.owner, + exclusive: matches!(lock_info.lock_scope, LockScope::Exclusive), + } + }; + if lock_data + .owner_dav + .as_ref() + .is_some_and(|o| o.size() > self.core.dav.dead_property_size.unwrap_or(512)) + { + return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE)); + } + + self.in_memory_store() + .key_set( + KeyValue::new( + resource_hash, + Archiver::new(lock_data) + .serialize() + .caused_by(trc::location!())?, + ) + .expires(timeout), + ) + .await + .caused_by(trc::location!())?; + } else if lock_data.is_some() { + self.in_memory_store() + .key_delete(resource_hash.as_slice()) + .await + .caused_by(trc::location!())?; + } + + todo!() + } +} + +#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] +struct LockData { + owner: u32, + depth_infinity: bool, + exclusive: bool, + owner_dav: Option, +} + +impl UriResource> { + pub fn lock_key(&self) -> Option> { + let mut hasher = blake3::Hasher::new(); + hasher.update(self.resource?.as_bytes()); + hasher.update(self.account_id?.to_be_bytes().as_slice()); + hasher.update(u8::from(self.collection).to_be_bytes().as_slice()); + let hash = hasher.finalize(); + let mut result = Vec::with_capacity(hash.as_bytes().len() + 1); + result.push(KV_LOCK_DAV); + result.extend_from_slice(hash.as_bytes()); + Some(result) + } +} diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index 9a26ba36..0ac4d696 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -1,2 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + pub mod acl; +pub mod lock; pub mod uri; diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index 72efc001..34c6100c 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + use common::{Server, auth::AccessToken}; use directory::backend::internal::manage::ManageDirectory; @@ -85,13 +91,3 @@ impl UriResource { self.account_id.ok_or(DavError::Code(StatusCode::FORBIDDEN)) } } - -impl UriResource> { - pub fn unwrap(self) -> UriResource { - UriResource { - collection: self.collection, - account_id: self.account_id, - resource: self.resource.unwrap(), - } - } -} diff --git a/crates/dav/src/file/copy_move.rs b/crates/dav/src/file/copy_move.rs index 194e96e4..459e24d1 100644 --- a/crates/dav/src/file/copy_move.rs +++ b/crates/dav/src/file/copy_move.rs @@ -6,12 +6,18 @@ use std::sync::Arc; -use common::{Files, Server, auth::AccessToken}; +use common::{Files, Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; use dav_proto::{Depth, RequestHeaders}; -use groupware::file::hierarchy::FileHierarchy; +use groupware::file::{FileNode, hierarchy::FileHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{ + acl::Acl, collection::Collection, property::Property, type_state::DataType, +}; +use store::{ + ahash::AHashMap, + write::{Archive, BatchBuilder, assert::HashedValue, log::ChangeLogBuilder, now}, +}; use trc::AddContext; use utils::map::bitmap::Bitmap; @@ -21,9 +27,11 @@ use crate::{ acl::DavAclHandler, uri::{DavUriResource, UriResource}, }, - file::{DavFileResource, FileItemId}, + file::{DavFileResource, FileItemId, insert_file_node, update_file_node}, }; +use super::{FromFileItem, delete_file_node}; + pub(crate) trait FileCopyMoveRequestHandler: Sync + Send { fn handle_file_copy_move_request( &self, @@ -84,7 +92,7 @@ impl FileCopyMoveRequestHandler for Server { .await?; // Validate destination - let to_resource = self + let destination = self .validate_uri( access_token, headers @@ -92,7 +100,10 @@ impl FileCopyMoveRequestHandler for Server { .ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?, ) .await?; - let to_account_id = to_resource + if destination.collection != Collection::FileNode { + return Err(DavError::Code(StatusCode::BAD_GATEWAY)); + } + let to_account_id = destination .account_id .ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?; let to_files = if to_account_id == from_account_id { @@ -102,23 +113,58 @@ impl FileCopyMoveRequestHandler for Server { .await .caused_by(trc::location!())? }; - let to_resource = to_files.map_destination::(to_resource)?; - if from_resource.collection != to_resource.collection - || (from_resource.account_id == to_resource.account_id - && to_resource - .resource - .as_ref() - .is_some_and(|r| r.document_id == from_resource.resource.document_id)) - { - return Err(DavError::Code(StatusCode::BAD_GATEWAY)); + + // Map file item + let mut destination = if let Some(resource) = destination.resource { + // Check if the resource exists + if let Some(destination) = to_files + .files + .by_name(resource) + .map(Destination::from_file_item) + { + destination + } else if let Some((destination, new_name)) = + to_files.map_parent::(resource) + { + let mut destination = destination.unwrap_or_default(); + destination.new_name = Some(new_name.into_owned()); + destination + } else { + return Err(DavError::Code(StatusCode::BAD_GATEWAY)); + } + } else { + Destination::default() + }; + destination.account_id = to_account_id; + + if from_account_id == destination.account_id { + if Some(from_resource.resource.document_id) == destination.document_id { + // Move or copy to the same location + return Ok(HttpResponse::new(StatusCode::BAD_GATEWAY)); + } else if from_resource.resource.parent_id == destination.parent_id + && destination.new_name.is_some() + { + // Rename + self.validate_child_or_parent_acl( + access_token, + from_account_id, + Collection::FileNode, + from_resource.resource.document_id, + from_resource.resource.parent_id, + Acl::Modify, + Acl::ModifyItems, + ) + .await?; + return rename_item(self, access_token, from_resource, destination).await; + } } // Validate destination ACLs - if let Some(to_resource) = &to_resource.resource { + if let Some(document_id) = destination.document_id { let mut child_acl = Bitmap::new(); - if to_resource.is_container { - child_acl.insert(Acl::ModifyItems); + if destination.is_container { + child_acl.insert(Acl::AddItems); } else { child_acl.insert(Acl::Modify); } @@ -127,28 +173,46 @@ impl FileCopyMoveRequestHandler for Server { access_token, to_account_id, Collection::FileNode, - to_resource.document_id, - to_resource.parent_id, + document_id, + destination.parent_id, child_acl, - Acl::ModifyItems, + Acl::AddItems, ) .await?; } else if !access_token.is_member(to_account_id) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } + // Validate quota + if !is_move || from_account_id != to_account_id { + let res = from_files + .files + .by_id(from_resource.resource.document_id) + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + let space_needed = from_files + .subtree(&res.name) + .map(|a| a.size as u64) + .sum::(); + self.has_available_quota( + &self.get_resource_token(access_token, to_account_id).await?, + space_needed, + ) + .await?; + } + match ( from_resource.resource.is_container, - to_resource.resource.as_ref().is_none_or(|r| r.is_container), + destination.is_container, is_move, ) { (true, true, true) => { move_container( self, + access_token, from_files, to_files, from_resource, - to_resource, + destination, headers.depth, ) .await @@ -156,71 +220,542 @@ impl FileCopyMoveRequestHandler for Server { (true, true, false) => { copy_container( self, + access_token, from_files, - to_files, from_resource, - to_resource, + destination, headers.depth, + false, ) .await } - (false, false, true) => replace_item(from_resource, to_resource.unwrap()).await, - (false, false, false) => overwrite_item(from_resource, to_resource.unwrap()).await, - (false, true, true) => move_item(from_resource, to_resource).await, - (false, true, false) => copy_item(from_resource, to_resource).await, + (false, false, true) => { + overwrite_and_delete_item(self, access_token, from_resource, destination).await + } + (false, false, false) => { + overwrite_item(self, access_token, from_resource, destination).await + } + (false, true, true) => move_item(self, access_token, from_resource, destination).await, + (false, true, false) => copy_item(self, access_token, from_resource, destination).await, _ => Err(DavError::Code(StatusCode::BAD_GATEWAY)), } } } +pub(crate) struct Destination { + pub account_id: u32, + pub new_name: Option, + pub document_id: Option, + pub parent_id: Option, + pub is_container: bool, +} + +impl Default for Destination { + fn default() -> Self { + Self { + account_id: Default::default(), + document_id: Default::default(), + parent_id: Default::default(), + new_name: Default::default(), + is_container: true, + } + } +} + +// Moves a container under an existing container async fn move_container( server: &Server, + access_token: &AccessToken, from_files: Arc, to_files: Arc, from_resource: UriResource, - to_resource: UriResource>, + destination: Destination, depth: Depth, ) -> crate::Result { - // check ancestors - todo!() + let from_account_id = from_resource.account_id.unwrap(); + 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); + + if from_account_id == to_account_id { + if parent_id != 0 && to_files.is_ancestor_of(from_document_id, parent_id - 1) { + return Err(DavError::Code(StatusCode::BAD_GATEWAY)); + } + let node = server + .get_property::>( + from_account_id, + Collection::FileNode, + from_document_id, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))? + .into_deserialized::() + .caused_by(trc::location!())?; + let mut new_node = node.inner.clone(); + new_node.parent_id = parent_id; + if let Some(new_name) = destination.new_name { + new_node.name = new_name; + } + update_file_node( + server, + access_token, + node, + new_node, + from_account_id, + from_document_id, + ) + .await + .caused_by(trc::location!())?; + + Ok(HttpResponse::new(StatusCode::CREATED)) + } else { + copy_container( + server, + access_token, + from_files, + from_resource, + destination, + depth, + true, + ) + .await + } } async fn copy_container( server: &Server, + access_token: &AccessToken, from_files: Arc, - to_files: Arc, from_resource: UriResource, - to_resource: UriResource>, + mut destination: Destination, depth: Depth, + delete_source: bool, ) -> crate::Result { - // check ancestors - todo!() + let infinity_copy = match depth { + Depth::Zero => { + return copy_item(server, access_token, from_resource, destination).await; + } + Depth::One => false, + _ => true, + }; + + let from_account_id = from_resource.account_id.unwrap(); + 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 + .files + .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)) + .collect::>() + } else { + from_files + .subtree_with_depth(&res.name, 1) + .map(|r| (r.document_id, r.hierarchy_sequence)) + .collect::>() + }; + + // Top-down copy + let mut id_map = AHashMap::with_capacity(copy_files.len()); + let mut delete_files = if delete_source { + Vec::with_capacity(copy_files.len()) + } else { + Vec::new() + }; + copy_files.sort_unstable_by(|a, b| a.1.cmp(&b.1)); + let change_id = server.generate_snowflake_id()?; + let mut changes = ChangeLogBuilder::with_change_id(change_id); + let now = now() as i64; + for (document_id, _) in copy_files.into_iter() { + let node_ = server + .get_property::>( + from_account_id, + Collection::FileNode, + document_id, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))? + .into_deserialized::() + .caused_by(trc::location!())?; + + // Build node + let mut node = if !delete_source { + node_.inner + } else { + let node = node_.inner.clone(); + delete_files.push((document_id, node_)); + node + }; + node.modified = now; + node.created = now; + if let Some(new_name) = destination.new_name.take() { + node.name = new_name; + } + node.parent_id = if let Some(&prev_document_id) = id_map.get(&node.parent_id) { + prev_document_id + } else { + parent_id + }; + + // Prepare write batch + let mut batch = BatchBuilder::new(); + batch + .with_change_id(change_id) + .with_account_id(to_account_id) + .with_collection(Collection::FileNode) + .create_document() + .custom( + ObjectIndexBuilder::<(), _>::new() + .with_changes(node) + .with_tenant_id(access_token), + ) + .caused_by(trc::location!())?; + let new_document_id = server + .store() + .write(batch) + .await + .caused_by(trc::location!())? + .last_document_id() + .caused_by(trc::location!())?; + changes.log_insert(Collection::FileNode, new_document_id); + id_map.insert(document_id + 1, new_document_id + 1); + } + + // Write changes + if !changes.is_empty() { + server + .commit_changes(to_account_id, changes) + .await + .caused_by(trc::location!())?; + server + .broadcast_single_state_change(to_account_id, change_id, DataType::FileNode) + .await; + } + + // Delete nodes + if !delete_files.is_empty() { + let mut changes = ChangeLogBuilder::with_change_id(change_id); + for (document_id, node) in delete_files.into_iter().rev() { + // Delete record + let mut batch = BatchBuilder::new(); + batch + .with_account_id(from_account_id) + .with_collection(Collection::FileNode) + .delete_document(document_id) + .custom( + ObjectIndexBuilder::<_, ()>::new() + .with_tenant_id(access_token) + .with_current(node), + ) + .caused_by(trc::location!())?; + server + .store() + .write(batch) + .await + .caused_by(trc::location!())?; + changes.log_delete(Collection::FileNode, document_id); + } + + // Write changes + if !changes.is_empty() { + server + .commit_changes(from_account_id, changes) + .await + .caused_by(trc::location!())?; + server + .broadcast_single_state_change(from_account_id, change_id, DataType::FileNode) + .await; + } + } + + Ok(HttpResponse::new(StatusCode::CREATED)) } -async fn replace_item( +// Overwrites the contents of one file with another, then deletes the original +async fn overwrite_and_delete_item( + server: &Server, + access_token: &AccessToken, from_resource: UriResource, - to_resource: UriResource, + destination: Destination, ) -> crate::Result { - todo!() + let from_account_id = from_resource.account_id.unwrap(); + let to_account_id = destination.account_id; + let from_document_id = from_resource.resource.document_id; + let to_document_id = destination.document_id.unwrap(); + + // dest_node is the current file at the destination + let dest_node = server + .get_property::>( + to_account_id, + Collection::FileNode, + to_document_id, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))? + .into_deserialized::() + .caused_by(trc::location!())?; + + // source_node is the file to be copied + let source_node_ = server + .get_property::>( + from_account_id, + Collection::FileNode, + from_document_id, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))? + .into_deserialized::() + .caused_by(trc::location!())?; + let mut source_node = source_node_.inner.clone(); + source_node.name = if let Some(new_name) = destination.new_name { + new_name + } else { + dest_node.inner.name.clone() + }; + source_node.parent_id = dest_node.inner.parent_id; + + update_file_node( + server, + access_token, + dest_node, + source_node, + to_account_id, + to_document_id, + ) + .await + .caused_by(trc::location!())?; + + delete_file_node( + server, + access_token, + source_node_, + from_account_id, + from_document_id, + ) + .await + .caused_by(trc::location!())?; + + Ok(HttpResponse::new(StatusCode::CREATED)) } +// Overwrites the contents of one file with another async fn overwrite_item( + server: &Server, + access_token: &AccessToken, from_resource: UriResource, - to_resource: UriResource, + destination: Destination, ) -> crate::Result { - todo!() + let from_account_id = from_resource.account_id.unwrap(); + let to_account_id = destination.account_id; + let from_document_id = from_resource.resource.document_id; + let to_document_id = destination.document_id.unwrap(); + + // dest_node is the current file at the destination + let dest_node = server + .get_property::>( + to_account_id, + Collection::FileNode, + to_document_id, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))? + .into_deserialized::() + .caused_by(trc::location!())?; + + // source_node is the file to be copied + let mut source_node = server + .get_property::( + from_account_id, + Collection::FileNode, + from_document_id, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))? + .deserialize::() + .caused_by(trc::location!())?; + source_node.name = if let Some(new_name) = destination.new_name { + new_name + } else { + dest_node.inner.name.clone() + }; + source_node.parent_id = dest_node.inner.parent_id; + + update_file_node( + server, + access_token, + dest_node, + source_node, + to_account_id, + to_document_id, + ) + .await + .caused_by(trc::location!())?; + + Ok(HttpResponse::new(StatusCode::CREATED)) } +// Moves an item under an existing container async fn move_item( + server: &Server, + access_token: &AccessToken, from_resource: UriResource, - to_resource: UriResource>, + destination: Destination, ) -> crate::Result { - todo!() + let from_account_id = from_resource.account_id.unwrap(); + 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); + + let node = server + .get_property::>( + from_account_id, + Collection::FileNode, + from_document_id, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))? + .into_deserialized::() + .caused_by(trc::location!())?; + let mut new_node = node.inner.clone(); + new_node.parent_id = parent_id; + if let Some(new_name) = destination.new_name { + new_node.name = new_name; + } + + if from_account_id == to_account_id { + // Destination is in the same account: just update the parent id + update_file_node( + server, + access_token, + node, + new_node, + from_account_id, + from_document_id, + ) + .await + .caused_by(trc::location!())?; + } else { + // Destination is in a different account: insert a new node, then delete the old one + insert_file_node(server, access_token, new_node, to_account_id) + .await + .caused_by(trc::location!())?; + delete_file_node( + server, + access_token, + node, + from_account_id, + from_document_id, + ) + .await + .caused_by(trc::location!())?; + } + + Ok(HttpResponse::new(StatusCode::CREATED)) } +// Copies an item under an existing container async fn copy_item( + server: &Server, + access_token: &AccessToken, from_resource: UriResource, - to_resource: UriResource>, + destination: Destination, ) -> crate::Result { - todo!() + let from_account_id = from_resource.account_id.unwrap(); + 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); + + let mut node = server + .get_property::( + from_account_id, + Collection::FileNode, + from_document_id, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))? + .deserialize::() + .caused_by(trc::location!())?; + node.parent_id = parent_id; + if let Some(new_name) = destination.new_name { + node.name = new_name; + } + insert_file_node(server, access_token, node, to_account_id) + .await + .caused_by(trc::location!())?; + + Ok(HttpResponse::new(StatusCode::CREATED)) +} + +// Renames an item +async fn rename_item( + server: &Server, + access_token: &AccessToken, + from_resource: UriResource, + destination: Destination, +) -> crate::Result { + let from_account_id = from_resource.account_id.unwrap(); + let from_document_id = from_resource.resource.document_id; + + let node = server + .get_property::>( + from_account_id, + Collection::FileNode, + from_document_id, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))? + .into_deserialized::() + .caused_by(trc::location!())?; + let mut new_node = node.inner.clone(); + if let Some(new_name) = destination.new_name { + new_node.name = new_name; + } + update_file_node( + server, + access_token, + node, + new_node, + from_account_id, + from_document_id, + ) + .await + .caused_by(trc::location!())?; + + Ok(HttpResponse::new(StatusCode::CREATED)) +} + +impl FromFileItem for Destination { + fn from_file_item(item: &common::FileItem) -> Self { + Destination { + account_id: u32::MAX, + document_id: Some(item.document_id), + parent_id: item.parent_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 3b92361b..35fbef4a 100644 --- a/crates/dav/src/file/delete.rs +++ b/crates/dav/src/file/delete.rs @@ -54,7 +54,7 @@ impl FileDeleteRequestHandler for Server { } // Sort ids descending from the deepest to the root - ids.sort_unstable_by(|a, b| b.name.len().cmp(&a.name.len())); + ids.sort_unstable_by(|a, b| b.hierarchy_sequence.cmp(&a.hierarchy_sequence)); let (document_id, parent_id, is_container) = ids .last() .map(|a| (a.document_id, a.parent_id, a.is_container)) @@ -83,7 +83,7 @@ impl FileDeleteRequestHandler for Server { // Process deletions let mut changes = ChangeLogBuilder::new(); for document_id in sorted_ids { - if let Some(submission) = self + if let Some(node) = self .get_property::>( account_id, Collection::FileNode, @@ -92,7 +92,7 @@ impl FileDeleteRequestHandler for Server { ) .await? { - // Update record + // Delete record let mut batch = BatchBuilder::new(); batch .with_account_id(account_id) @@ -102,8 +102,7 @@ impl FileDeleteRequestHandler for Server { ObjectIndexBuilder::<_, ()>::new() .with_tenant_id(access_token) .with_current( - submission - .to_unarchived::() + node.to_unarchived::() .caused_by(trc::location!())?, ), ) diff --git a/crates/dav/src/file/lock.rs b/crates/dav/src/file/lock.rs deleted file mode 100644 index 18e0dc63..00000000 --- a/crates/dav/src/file/lock.rs +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - - use common::{Server, auth::AccessToken}; -use dav_proto::{RequestHeaders, schema::request::LockInfo}; -use http_proto::HttpResponse; - -pub(crate) trait FileLockRequestHandler: Sync + Send { - fn handle_file_lock_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: Option, - ) -> impl Future> + Send; -} - -impl FileLockRequestHandler for Server { - async fn handle_file_lock_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: Option, - ) -> crate::Result { - todo!() - } -} diff --git a/crates/dav/src/file/mod.rs b/crates/dav/src/file/mod.rs index 81b8bd4a..301c4946 100644 --- a/crates/dav/src/file/mod.rs +++ b/crates/dav/src/file/mod.rs @@ -6,8 +6,16 @@ use std::borrow::Cow; -use common::{FileItem, Files}; +use common::{FileItem, Files, Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use groupware::file::FileNode; use hyper::StatusCode; +use jmap_proto::types::{collection::Collection, type_state::DataType}; +use store::write::{ + BatchBuilder, + assert::HashedValue, + log::{Changes, LogInsert}, + now, +}; use crate::{DavError, common::uri::UriResource}; @@ -16,7 +24,6 @@ pub mod changes; pub mod copy_move; pub mod delete; pub mod get; -pub mod lock; pub mod mkcol; pub mod propfind; pub mod proppatch; @@ -38,15 +45,10 @@ pub(crate) trait DavFileResource { resource: UriResource>, ) -> crate::Result>; - fn map_destination( - &self, - resource: UriResource>, - ) -> crate::Result>>; - fn map_parent<'x, T: FromFileItem>( &self, resource: &'x str, - ) -> crate::Result<(Option, Cow<'x, str>)>; + ) -> Option<(Option, Cow<'x, str>)>; fn map_parent_resource<'x, T: FromFileItem>( &self, @@ -70,45 +72,20 @@ impl DavFileResource for Files { .ok_or(DavError::Code(StatusCode::NOT_FOUND)) } - fn map_destination( - &self, - resource: UriResource>, - ) -> crate::Result>> { - Ok(UriResource { - collection: resource.collection, - account_id: resource.account_id, - resource: if let Some(resource) = resource.resource { - Some( - self.files - .by_name(resource) - .map(T::from_file_item) - .ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?, - ) - } else { - None - }, - }) - } - fn map_parent<'x, T: FromFileItem>( &self, resource: &'x str, - ) -> crate::Result<(Option, Cow<'x, str>)> { + ) -> Option<(Option, Cow<'x, str>)> { let (parent, child) = if let Some((parent, child)) = resource.rsplit_once('/') { ( - Some( - self.files - .by_name(parent) - .map(T::from_file_item) - .ok_or(DavError::Code(StatusCode::NOT_FOUND))?, - ), + Some(self.files.by_name(parent).map(T::from_file_item)?), child, ) } else { (None, resource) }; - Ok(( + Some(( parent, percent_encoding::percent_decode_str(child) .decode_utf8() @@ -122,11 +99,13 @@ impl DavFileResource for Files { ) -> crate::Result, Cow<'x, str>)>> { if let Some(r) = resource.resource { if self.files.by_name(r).is_none() { - self.map_parent(r).map(|r| UriResource { - collection: resource.collection, - account_id: resource.account_id, - resource: r, - }) + self.map_parent(r) + .map(|r| UriResource { + collection: resource.collection, + account_id: resource.account_id, + resource: r, + }) + .ok_or(DavError::Code(StatusCode::NOT_FOUND)) } else { Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) } @@ -151,3 +130,106 @@ impl FromFileItem for FileItemId { } } } + +pub(crate) async fn update_file_node( + server: &Server, + access_token: &AccessToken, + node: HashedValue, + mut new_node: FileNode, + account_id: u32, + document_id: u32, +) -> trc::Result<()> { + // Build node + new_node.modified = now() as i64; + new_node.change_id = server.generate_snowflake_id()?; + + // Prepare write batch + let mut batch = BatchBuilder::new(); + let change_id = new_node.change_id; + batch + .with_change_id(change_id) + .with_account_id(account_id) + .with_collection(Collection::FileNode) + .update_document(document_id) + .log(Changes::update([document_id])) + .custom( + ObjectIndexBuilder::new() + .with_current(node) + .with_changes(new_node) + .with_tenant_id(access_token), + )?; + server.store().write(batch).await?; + + // Broadcast state change + server + .broadcast_single_state_change(account_id, change_id, DataType::FileNode) + .await; + + Ok(()) +} + +pub(crate) async fn insert_file_node( + server: &Server, + access_token: &AccessToken, + mut node: FileNode, + account_id: u32, +) -> trc::Result<()> { + // Build node + let now = now() as i64; + node.modified = now; + node.created = now; + node.change_id = server.generate_snowflake_id()?; + + // Prepare write batch + let mut batch = BatchBuilder::new(); + let change_id = node.change_id; + batch + .with_change_id(change_id) + .with_account_id(account_id) + .with_collection(Collection::FileNode) + .create_document() + .log(LogInsert()) + .custom( + ObjectIndexBuilder::<(), _>::new() + .with_changes(node) + .with_tenant_id(access_token), + )?; + server.store().write(batch).await?; + + // Broadcast state change + server + .broadcast_single_state_change(account_id, change_id, DataType::FileNode) + .await; + + Ok(()) +} + +pub(crate) async fn delete_file_node( + server: &Server, + access_token: &AccessToken, + node: HashedValue, + account_id: u32, + document_id: u32, +) -> trc::Result<()> { + // Prepare write batch + let mut batch = BatchBuilder::new(); + let change_id = server.generate_snowflake_id()?; + batch + .with_change_id(change_id) + .with_account_id(account_id) + .with_collection(Collection::FileNode) + .create_document() + .log(Changes::delete([document_id])) + .custom( + ObjectIndexBuilder::<_, ()>::new() + .with_current(node) + .with_tenant_id(access_token), + )?; + server.store().write(batch).await?; + + // Broadcast state change + server + .broadcast_single_state_change(account_id, change_id, DataType::FileNode) + .await; + Ok(()) +} diff --git a/crates/dav/src/file/proppatch.rs b/crates/dav/src/file/proppatch.rs index 5b43e0b4..7aee32a7 100644 --- a/crates/dav/src/file/proppatch.rs +++ b/crates/dav/src/file/proppatch.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use common::{Server, auth::AccessToken}; use dav_proto::{ RequestHeaders, schema::{ @@ -16,10 +16,8 @@ use dav_proto::{ use groupware::file::{FileNode, hierarchy::FileHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{ - acl::Acl, collection::Collection, property::Property, type_state::DataType, -}; -use store::write::{Archive, BatchBuilder, assert::HashedValue, log::Changes, now}; +use jmap_proto::types::{acl::Acl, collection::Collection, property::Property}; +use store::write::{Archive, assert::HashedValue}; use trc::AddContext; use crate::{ @@ -28,6 +26,8 @@ use crate::{ file::{DavFileResource, acl::FileAclRequestHandler}, }; +use super::update_file_node; + pub(crate) trait FilePropPatchRequestHandler: Sync + Send { fn handle_file_proppatch_request( &self, @@ -127,34 +127,16 @@ impl FilePropPatchRequestHandler for Server { self.apply_file_properties(&mut new_node, true, request.set, &mut items); if new_node != node.inner { - // Build node - new_node.modified = now() as i64; - new_node.change_id = self.generate_snowflake_id().caused_by(trc::location!())?; - - // Prepare write batch - let mut batch = BatchBuilder::new(); - let change_id = new_node.change_id; - batch - .with_change_id(change_id) - .with_account_id(account_id) - .with_collection(Collection::FileNode) - .update_document(resource.resource) - .log(Changes::update([resource.resource])) - .custom( - ObjectIndexBuilder::new() - .with_current(node) - .with_changes(new_node) - .with_tenant_id(access_token), - ) - .caused_by(trc::location!())?; - self.store() - .write(batch) - .await - .caused_by(trc::location!())?; - - // Broadcast state change - self.broadcast_single_state_change(account_id, change_id, DataType::FileNode) - .await; + update_file_node( + self, + access_token, + node, + new_node, + account_id, + resource.resource, + ) + .await + .caused_by(trc::location!())?; } Ok(HttpResponse::new(StatusCode::MULTI_STATUS) diff --git a/crates/dav/src/file/update.rs b/crates/dav/src/file/update.rs index 4938b4d8..eb21eb19 100644 --- a/crates/dav/src/file/update.rs +++ b/crates/dav/src/file/update.rs @@ -98,8 +98,11 @@ impl FileUpdateRequestHandler for Server { let extra_bytes = (bytes.len() as u64) .saturating_sub(u32::from(node.file.as_ref().unwrap().size) as u64); if extra_bytes > 0 { - self.has_available_quota(&access_token.as_resource_token(), extra_bytes) - .await?; + self.has_available_quota( + &self.get_resource_token(access_token, account_id).await?, + extra_bytes, + ) + .await?; } // Write blob @@ -149,7 +152,9 @@ impl FileUpdateRequestHandler for Server { Ok(HttpResponse::new(StatusCode::OK)) } else { // Insert - let (parent_id, resource_name) = files.map_parent(resource_name)?; + let (parent_id, resource_name) = files + .map_parent(resource_name) + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; // Validate ACL let parent_id = self @@ -184,8 +189,11 @@ impl FileUpdateRequestHandler for Server { // Validate quota if !bytes.is_empty() { - self.has_available_quota(&access_token.as_resource_token(), bytes.len() as u64) - .await?; + self.has_available_quota( + &self.get_resource_token(access_token, account_id).await?, + bytes.len() as u64, + ) + .await?; } // Write blob diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index 56985fc0..74d7727c 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -11,7 +11,7 @@ pub mod file; pub mod principal; pub mod request; -use dav_proto::schema::{request::Report, response::Condition}; +use dav_proto::schema::response::Condition; use http_proto::HttpResponse; use hyper::{Method, StatusCode}; use jmap_proto::types::collection::Collection; diff --git a/crates/dav/src/request.rs b/crates/dav/src/request.rs index 500f6cb1..8f009912 100644 --- a/crates/dav/src/request.rs +++ b/crates/dav/src/request.rs @@ -22,10 +22,11 @@ use hyper::{StatusCode, header}; use crate::{ DavError, DavMethod, DavResource, + common::lock::LockRequestHandler, file::{ acl::FileAclRequestHandler, changes::FileChangesRequestHandler, copy_move::FileCopyMoveRequestHandler, delete::FileDeleteRequestHandler, - get::FileGetRequestHandler, lock::FileLockRequestHandler, mkcol::FileMkColRequestHandler, + get::FileGetRequestHandler, mkcol::FileMkColRequestHandler, propfind::FilePropFindRequestHandler, proppatch::FilePropPatchRequestHandler, update::FileUpdateRequestHandler, }, @@ -70,18 +71,13 @@ impl DavRequestDispatcher for Server { // Dispatch let todo = "lock tokens, headers, etc"; - match resource { - DavResource::Card => { - todo!() - } - DavResource::Cal => { - todo!() - } - DavResource::Principal => { - todo!() - } - DavResource::File => match method { - DavMethod::PROPFIND => { + + match method { + DavMethod::PROPFIND => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::Principal => todo!(), + DavResource::File => { self.handle_file_propfind_request( &access_token, headers, @@ -89,7 +85,12 @@ impl DavRequestDispatcher for Server { ) .await } - DavMethod::PROPPATCH => { + }, + DavMethod::PROPPATCH => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::Principal => todo!(), + DavResource::File => { self.handle_file_proppatch_request( &access_token, headers, @@ -97,7 +98,12 @@ impl DavRequestDispatcher for Server { ) .await } - DavMethod::MKCOL => { + }, + DavMethod::MKCOL => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::Principal => todo!(), + DavResource::File => { self.handle_file_mkcol_request( &access_token, headers, @@ -109,63 +115,108 @@ impl DavRequestDispatcher for Server { ) .await } - DavMethod::GET => { + }, + DavMethod::GET => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::Principal => todo!(), + DavResource::File => { self.handle_file_get_request(&access_token, headers, false) .await } - DavMethod::HEAD => { + }, + DavMethod::HEAD => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::Principal => todo!(), + DavResource::File => { self.handle_file_get_request(&access_token, headers, true) .await } - DavMethod::DELETE => { + }, + DavMethod::DELETE => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::Principal => todo!(), + DavResource::File => { self.handle_file_delete_request(&access_token, headers) .await } - DavMethod::PUT | DavMethod::POST => { + }, + DavMethod::PUT | DavMethod::POST => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::Principal => todo!(), + DavResource::File => { self.handle_file_update_request(&access_token, headers, body, false) .await } - DavMethod::PATCH => { + }, + DavMethod::PATCH => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::Principal => todo!(), + DavResource::File => { self.handle_file_update_request(&access_token, headers, body, true) .await } - DavMethod::COPY => { + }, + DavMethod::COPY => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::Principal => todo!(), + DavResource::File => { self.handle_file_copy_move_request(&access_token, headers, false) .await } - DavMethod::MOVE => { + }, + DavMethod::MOVE => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::Principal => todo!(), + DavResource::File => { self.handle_file_copy_move_request(&access_token, headers, true) .await } - DavMethod::LOCK => { - self.handle_file_lock_request( + }, + DavMethod::LOCK => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::Principal => todo!(), + DavResource::File => { + self.handle_lock_request( &access_token, headers, LockInfo::parse(&mut Tokenizer::new(&body))?.into(), ) .await } - DavMethod::UNLOCK => { - self.handle_file_lock_request(&access_token, headers, None) + }, + DavMethod::UNLOCK => self.handle_lock_request(&access_token, headers, None).await, + DavMethod::ACL => { + self.handle_file_acl_request( + &access_token, + headers, + Acl::parse(&mut Tokenizer::new(&body))?, + ) + .await + } + DavMethod::REPORT => match Report::parse(&mut Tokenizer::new(&body))? { + Report::SyncCollection(sync_collection) => { + self.handle_file_changes_request(&access_token, headers, sync_collection) .await } - DavMethod::ACL => { - self.handle_file_acl_request( - &access_token, - headers, - Acl::parse(&mut Tokenizer::new(&body))?, - ) - .await - } - DavMethod::REPORT => match Report::parse(&mut Tokenizer::new(&body))? { - Report::SyncCollection(sync_collection) => { - self.handle_file_changes_request(&access_token, headers, sync_collection) - .await - } - _ => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), - }, - DavMethod::OPTIONS => unreachable!(), + Report::Addressbook(addressbook_query) => todo!(), + Report::AdressbookMultiGet(multi_get) => todo!(), + Report::CalendarQuery(calendar_query) => todo!(), + Report::CalendarMultiGet(multi_get) => todo!(), + Report::FreeBusyQuery(free_busy_query) => todo!(), + Report::AclPrincipalPropSet(acl_principal_prop_set) => todo!(), + Report::PrincipalMatch(principal_match) => todo!(), + Report::PrincipalPropertySearch(principal_property_search) => todo!(), + Report::PrincipalSearchPropertySet => todo!(), }, + DavMethod::OPTIONS => unreachable!(), } } } diff --git a/crates/email/src/mailbox/index.rs b/crates/email/src/mailbox/index.rs index 89f21aa3..d022d627 100644 --- a/crates/email/src/mailbox/index.rs +++ b/crates/email/src/mailbox/index.rs @@ -110,6 +110,10 @@ impl FolderHierarchy for ArchivedMailbox { fn is_container(&self) -> bool { true } + + fn size(&self) -> u32 { + 0 + } } impl From<&UidMailbox> for TagValue { diff --git a/crates/email/src/mailbox/manage.rs b/crates/email/src/mailbox/manage.rs index 314e68df..655e4b41 100644 --- a/crates/email/src/mailbox/manage.rs +++ b/crates/email/src/mailbox/manage.rs @@ -125,12 +125,12 @@ impl MailboxFnc for Server { .fetch_folders::(account_id, Collection::Mailbox) .await .caused_by(trc::location!())? - .format(|mailbox_id, name| { - if mailbox_id == INBOX_ID { - Some("inbox".to_string()) + .format(|f| { + f.name = if f.document_id == INBOX_ID { + "inbox".to_string() } else { - Some(name.to_lowercase()) - } + f.name.to_lowercase() + }; }) .into_iterator() .map(|e| (e.name, e.document_id)) @@ -273,7 +273,11 @@ impl MailboxFnc for Server { .await .map(|folders| { folders - .format(|mailbox_id, _| (mailbox_id == INBOX_ID).then(|| "INBOX".to_string())) + .format(|f| { + if f.document_id == INBOX_ID { + f.name = "INBOX".to_string(); + } + }) .into_iterator() .find(|e| e.name.eq_ignore_ascii_case(path)) .map(|e| e.document_id) diff --git a/crates/groupware/src/file/hierarchy.rs b/crates/groupware/src/file/hierarchy.rs index 1b4b9224..e6dbb13c 100644 --- a/crates/groupware/src/file/hierarchy.rs +++ b/crates/groupware/src/file/hierarchy.rs @@ -51,10 +51,8 @@ async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result(account_id, Collection::FileNode) .await .caused_by(trc::location!())? - .format(|_, name| { - percent_encoding::utf8_percent_encode(name, NON_ALPHANUMERIC) - .to_string() - .into() + .format(|f| { + f.name = percent_encoding::utf8_percent_encode(&f.name, NON_ALPHANUMERIC).to_string(); }); let mut files = Files { files: IdBimap::with_capacity(list.len()), @@ -70,7 +68,9 @@ async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result 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([ @@ -81,8 +77,8 @@ impl IndexableObject for &ArchivedFileNode { }, ]); + let size = self.size(); if let Some(file) = self.file.as_ref() { - let size = size + file.size; values.extend([ IndexValue::Blob { value: (&file.blob_hash).into(), @@ -115,4 +111,11 @@ impl FolderHierarchy for ArchivedFileNode { fn is_container(&self) -> bool { self.file.is_none() } + + 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| u32::from(f.size)) + } }