Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
api::acl::JmapAcl,
|
||||
blob::download::BlobDownload,
|
||||
changes::state::JmapCacheState,
|
||||
file::set::{
|
||||
Collision, NoResolver, fetch_existing_modified, find_sibling_collision, pick_unique_rename,
|
||||
update_file_node, validate_file_node_hierarchy,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use groupware::{cache::GroupwareCache, file::FileNode};
|
||||
use http_proto::HttpSessionData;
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
method::{
|
||||
copy::{CopyRequest, CopyResponse},
|
||||
set::SetRequest,
|
||||
},
|
||||
object::file_node::{self, FileNodeProperty, OnExists},
|
||||
request::{
|
||||
Call, IntoValid, MaybeInvalid, RequestMethod, SetRequestMethod,
|
||||
method::{MethodFunction, MethodName, MethodObject},
|
||||
reference::MaybeResultReference,
|
||||
},
|
||||
types::state::State,
|
||||
};
|
||||
use store::{
|
||||
ValueKey,
|
||||
ahash::{AHashMap, AHashSet},
|
||||
roaring::RoaringBitmap,
|
||||
write::{AlignedBytes, Archive, BatchBuilder, now},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub trait FileNodeCopy: Sync + Send {
|
||||
fn file_node_copy<'x>(
|
||||
&self,
|
||||
request: CopyRequest<'x, file_node::FileNode>,
|
||||
access_token: &AccessToken,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = trc::Result<CopyResponse<file_node::FileNode>>> + Send;
|
||||
}
|
||||
|
||||
impl FileNodeCopy for Server {
|
||||
async fn file_node_copy<'x>(
|
||||
&self,
|
||||
request: CopyRequest<'x, file_node::FileNode>,
|
||||
access_token: &AccessToken,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
_session: &HttpSessionData,
|
||||
) -> trc::Result<CopyResponse<file_node::FileNode>> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let from_account_id = request.from_account_id.document_id();
|
||||
|
||||
if account_id == from_account_id {
|
||||
return Err(trc::JmapEvent::InvalidArguments
|
||||
.into_err()
|
||||
.details("From accountId is equal to fromAccountId"));
|
||||
}
|
||||
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let old_state = cache.assert_state(false, &request.if_in_state)?;
|
||||
let mut response = CopyResponse {
|
||||
from_account_id: request.from_account_id,
|
||||
account_id: request.account_id,
|
||||
new_state: old_state.clone(),
|
||||
old_state,
|
||||
created: VecMap::with_capacity(request.create.len()),
|
||||
not_created: VecMap::new(),
|
||||
};
|
||||
|
||||
let from_cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
from_account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let from_node_ids = if access_token.is_member(from_account_id) {
|
||||
from_cache
|
||||
.resources
|
||||
.iter()
|
||||
.map(|r| r.document_id)
|
||||
.collect::<RoaringBitmap>()
|
||||
} else {
|
||||
let mut readable =
|
||||
from_cache.shared_containers(access_token, [Acl::Read, Acl::ReadItems], true);
|
||||
readable |= from_cache.shared_items(access_token, [Acl::ReadItems], true);
|
||||
readable
|
||||
};
|
||||
|
||||
let is_shared = access_token.is_shared(account_id);
|
||||
let can_add_to = if is_shared {
|
||||
Some(cache.shared_containers(access_token, [Acl::AddItems], true))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let on_exists = request.arguments.on_exists;
|
||||
let case_insensitive = request
|
||||
.arguments
|
||||
.compare_case_insensitively
|
||||
.unwrap_or(false);
|
||||
let on_destroy_remove_children = request
|
||||
.arguments
|
||||
.on_destroy_remove_children
|
||||
.unwrap_or(false);
|
||||
let on_success_delete = request.on_success_destroy_original.unwrap_or(false);
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
let mut pending_names: AHashMap<(u32, String), Option<u32>> = AHashMap::new();
|
||||
let mut implicit_destroys: AHashSet<u32> = AHashSet::new();
|
||||
let mut created_folders = AHashMap::new();
|
||||
let mut destroy_ids = Vec::new();
|
||||
|
||||
'create: for (id, create) in request.create.into_valid() {
|
||||
let from_document_id = id.document_id();
|
||||
if !from_node_ids.contains(from_document_id) {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::not_found().with_description(format!(
|
||||
"Item {} not found in account {}.",
|
||||
id, response.from_account_id
|
||||
)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(source) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
from_account_id,
|
||||
Collection::FileNode,
|
||||
from_document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::not_found().with_description(format!(
|
||||
"Item {} not found in account {}.",
|
||||
id, response.from_account_id
|
||||
)),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut file_node = source
|
||||
.deserialize::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
// ACLs are account-scoped; do not carry the source account's grants over.
|
||||
file_node.acls.clear();
|
||||
|
||||
let has_acl_changes =
|
||||
match update_file_node(None, create, &mut file_node, true, &NoResolver) {
|
||||
Ok(result) => {
|
||||
if let Some(blob_id) = result.blob_id {
|
||||
let file_details = file_node.file.get_or_insert_default();
|
||||
if !self.has_access_blob(&blob_id, access_token).await? {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::forbidden().with_description(format!(
|
||||
"You do not have access to blobId {blob_id}."
|
||||
)),
|
||||
);
|
||||
continue 'create;
|
||||
} else if let Some(blob_contents) = self
|
||||
.blob_store()
|
||||
.get_blob(blob_id.hash.as_slice(), 0..usize::MAX)
|
||||
.await?
|
||||
{
|
||||
file_details.size = blob_contents.len() as u32;
|
||||
} else {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(FileNodeProperty::BlobId)
|
||||
.with_description("Blob could not be found."),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
file_details.blob_hash = blob_id.hash;
|
||||
}
|
||||
|
||||
if file_node
|
||||
.file
|
||||
.as_ref()
|
||||
.is_some_and(|f| f.blob_hash.is_empty())
|
||||
{
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(FileNodeProperty::BlobId)
|
||||
.with_description("Missing blob id."),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
result.has_acl_changes
|
||||
}
|
||||
Err(err) => {
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) =
|
||||
validate_file_node_hierarchy(None, &file_node, is_shared, &cache, &created_folders)
|
||||
{
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
if file_node.modified == 0 {
|
||||
file_node.modified = now() as i64;
|
||||
}
|
||||
|
||||
let renamed = match find_sibling_collision(
|
||||
None,
|
||||
&file_node,
|
||||
&cache,
|
||||
&pending_names,
|
||||
case_insensitive,
|
||||
) {
|
||||
Collision::None => false,
|
||||
Collision::Existing(existing) => {
|
||||
let effective = match on_exists {
|
||||
OnExists::Newest => {
|
||||
let existing_modified =
|
||||
fetch_existing_modified(self.store(), account_id, existing).await?;
|
||||
if file_node.modified > existing_modified {
|
||||
OnExists::Replace
|
||||
} else {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::already_exists()
|
||||
.with_existing_id(types::id::Id::from(existing)),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
match effective {
|
||||
OnExists::Reject => {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::already_exists()
|
||||
.with_existing_id(types::id::Id::from(existing)),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
OnExists::Rename => {
|
||||
file_node.name = pick_unique_rename(
|
||||
&file_node.name,
|
||||
None,
|
||||
file_node.parent_id,
|
||||
&cache,
|
||||
&pending_names,
|
||||
case_insensitive,
|
||||
);
|
||||
true
|
||||
}
|
||||
OnExists::Replace => {
|
||||
if let Some(target) = cache.any_resource_path_by_id(existing) {
|
||||
let subtree_len = cache.subtree(target.path()).count();
|
||||
if subtree_len > 1 && !on_destroy_remove_children {
|
||||
response
|
||||
.not_created
|
||||
.append(id, SetError::node_has_children());
|
||||
continue 'create;
|
||||
}
|
||||
}
|
||||
implicit_destroys.insert(existing);
|
||||
false
|
||||
}
|
||||
OnExists::Newest => unreachable!(),
|
||||
}
|
||||
}
|
||||
Collision::Pending => match on_exists {
|
||||
OnExists::Rename => {
|
||||
file_node.name = pick_unique_rename(
|
||||
&file_node.name,
|
||||
None,
|
||||
file_node.parent_id,
|
||||
&cache,
|
||||
&pending_names,
|
||||
case_insensitive,
|
||||
);
|
||||
true
|
||||
}
|
||||
OnExists::Reject | OnExists::Replace | OnExists::Newest => {
|
||||
let key = crate::file::set::pending_key(&file_node, case_insensitive);
|
||||
let mut err = SetError::already_exists();
|
||||
if let Some(Some(doc_id)) = pending_names.get(&key) {
|
||||
err = err.with_existing_id(types::id::Id::from(*doc_id));
|
||||
}
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Permission and ACL inheritance for the destination parent
|
||||
if file_node.parent_id > 0 {
|
||||
let parent_id = file_node.parent_id - 1;
|
||||
|
||||
// The user must be allowed to add children to the destination parent
|
||||
if let Some(allowed) = &can_add_to
|
||||
&& !created_folders.contains_key(&parent_id)
|
||||
&& !allowed.contains(parent_id)
|
||||
{
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::forbidden().with_description(
|
||||
"You are not allowed to create file nodes in this folder.",
|
||||
),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
let parent_acls = created_folders.get(&parent_id).cloned().or_else(|| {
|
||||
cache
|
||||
.container_resource_by_id(parent_id)
|
||||
.and_then(|r| r.acls())
|
||||
.map(|a| a.to_vec())
|
||||
});
|
||||
if !has_acl_changes {
|
||||
if let Some(parent_acls) = parent_acls {
|
||||
file_node.acls = parent_acls;
|
||||
}
|
||||
} else if is_shared
|
||||
&& parent_acls
|
||||
.is_none_or(|acls| !acls.effective_acl(access_token).contains(Acl::Share))
|
||||
{
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::forbidden()
|
||||
.with_description("You are not allowed to share this file node."),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
} else if is_shared {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::forbidden()
|
||||
.with_description("Cannot create top-level folder in a shared account."),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
if !file_node.acls.is_empty() {
|
||||
if let Err(err) = self.acl_validate(&file_node.acls).await {
|
||||
response.not_created.append(id, err.into());
|
||||
continue 'create;
|
||||
}
|
||||
self.refresh_acls(&file_node.acls, None)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::FileNode, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if file_node.file.is_none() {
|
||||
created_folders.insert(document_id, file_node.acls.clone());
|
||||
}
|
||||
pending_names.insert(
|
||||
crate::file::set::pending_key(&file_node, case_insensitive),
|
||||
None,
|
||||
);
|
||||
let final_name = file_node.name.clone();
|
||||
let set_created = file_node.created == 0;
|
||||
let set_modified = file_node.modified == 0;
|
||||
file_node
|
||||
.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
set_created,
|
||||
set_modified,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
response.created(id, document_id);
|
||||
if renamed
|
||||
&& let Some(value) = response.created.get_mut(&id)
|
||||
&& let jmap_tools::Value::Object(map) = value
|
||||
{
|
||||
map.insert_unchecked(
|
||||
jmap_tools::Key::Property(FileNodeProperty::Name),
|
||||
jmap_tools::Value::Str(std::borrow::Cow::Owned(final_name)),
|
||||
);
|
||||
}
|
||||
|
||||
if on_success_delete {
|
||||
destroy_ids.push(MaybeInvalid::Value(id));
|
||||
}
|
||||
}
|
||||
|
||||
for did in &implicit_destroys {
|
||||
let Some(node) = cache.any_resource_path_by_id(*did) else {
|
||||
continue;
|
||||
};
|
||||
let mut ids = cache.subtree(node.path()).collect::<Vec<_>>();
|
||||
ids.sort_unstable_by_key(|b| std::cmp::Reverse(b.hierarchy_seq()));
|
||||
let sorted = ids.into_iter().map(|a| a.document_id()).collect::<Vec<_>>();
|
||||
groupware::DestroyArchive(sorted)
|
||||
.delete_batch(
|
||||
self,
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
cache.format_resource(node).into(),
|
||||
&mut batch,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
if !batch.is_empty() {
|
||||
let change_id = self
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.and_then(|ids| ids.last_change_id(account_id))
|
||||
.caused_by(trc::location!())?;
|
||||
response.new_state = State::Exact(change_id);
|
||||
}
|
||||
|
||||
if on_success_delete && !destroy_ids.is_empty() {
|
||||
*next_call = Call {
|
||||
id: String::new(),
|
||||
name: MethodName::new(MethodObject::FileNode, MethodFunction::Set),
|
||||
method: RequestMethod::Set(SetRequestMethod::FileNode(Box::new(SetRequest {
|
||||
account_id: request.from_account_id,
|
||||
if_in_state: request.destroy_from_if_in_state,
|
||||
create: None,
|
||||
update: None,
|
||||
destroy: MaybeResultReference::Value(destroy_ids).into(),
|
||||
arguments: Default::default(),
|
||||
}))),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{api::acl::JmapRights, changes::state::JmapCacheState};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use groupware::{cache::GroupwareCache, file::FileNode};
|
||||
use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse},
|
||||
object::file_node::{self, FileNodeNodeType, FileNodeProperty, FileNodeValue},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use jmap_tools::{Map, Value};
|
||||
use store::{
|
||||
ValueKey,
|
||||
roaring::RoaringBitmap,
|
||||
write::{AlignedBytes, Archive, now},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::{Acl, AclGrant},
|
||||
blob::{BlobClass, BlobId},
|
||||
blob_hash::BlobHash,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub trait FileNodeGet: Sync + Send {
|
||||
fn file_node_get(
|
||||
&self,
|
||||
request: GetRequest<file_node::FileNode>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<GetResponse<file_node::FileNode>>> + Send;
|
||||
}
|
||||
|
||||
impl FileNodeGet for Server {
|
||||
async fn file_node_get(
|
||||
&self,
|
||||
mut request: GetRequest<file_node::FileNode>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<GetResponse<file_node::FileNode>> {
|
||||
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
FileNodeProperty::Id,
|
||||
FileNodeProperty::ParentId,
|
||||
FileNodeProperty::NodeType,
|
||||
FileNodeProperty::BlobId,
|
||||
FileNodeProperty::Target,
|
||||
FileNodeProperty::Size,
|
||||
FileNodeProperty::Name,
|
||||
FileNodeProperty::Type,
|
||||
FileNodeProperty::Created,
|
||||
FileNodeProperty::Modified,
|
||||
FileNodeProperty::Accessed,
|
||||
FileNodeProperty::Changed,
|
||||
FileNodeProperty::Executable,
|
||||
FileNodeProperty::IsSubscribed,
|
||||
FileNodeProperty::MyRights,
|
||||
FileNodeProperty::ShareWith,
|
||||
FileNodeProperty::Role,
|
||||
]);
|
||||
let account_id = request.account_id.document_id();
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await?;
|
||||
// TODO: draft-14 section 5 case 2 - ancestors of shared nodes should be discoverable with mayRead=false
|
||||
let file_node_ids = if access_token.is_member(account_id) {
|
||||
cache
|
||||
.resources
|
||||
.iter()
|
||||
.map(|r| r.document_id)
|
||||
.collect::<RoaringBitmap>()
|
||||
} else {
|
||||
cache.shared_documents(access_token, [Acl::Read, Acl::ReadItems], true)
|
||||
};
|
||||
|
||||
let mut ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
file_node_ids
|
||||
.iter()
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
if request.arguments.fetch_parents.unwrap_or(false) {
|
||||
let mut seen: RoaringBitmap = ids.iter().map(|i| i.document_id()).collect();
|
||||
let mut extra: Vec<types::id::Id> = Vec::new();
|
||||
for id in &ids {
|
||||
let mut current = cache
|
||||
.any_resource_path_by_id(id.document_id())
|
||||
.and_then(|r| r.parent_id());
|
||||
while let Some(parent_id) = current {
|
||||
if !seen.insert(parent_id) {
|
||||
break;
|
||||
}
|
||||
if file_node_ids.contains(parent_id) {
|
||||
extra.push(parent_id.into());
|
||||
}
|
||||
current = cache
|
||||
.container_resource_by_id(parent_id)
|
||||
.and_then(|r| r.parent_id());
|
||||
}
|
||||
}
|
||||
ids.extend(extra);
|
||||
}
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
state: cache.get_state(false).into(),
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: not_found_ids,
|
||||
};
|
||||
|
||||
for id in ids {
|
||||
// Obtain the file_node object
|
||||
let document_id = id.document_id();
|
||||
if !file_node_ids.contains(document_id) {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
}
|
||||
let _file_node = if let Some(file_node) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::FileNode,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
file_node
|
||||
} else {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
};
|
||||
let file_node = _file_node
|
||||
.unarchive::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut result = Map::with_capacity(properties.len());
|
||||
for property in &properties {
|
||||
match property {
|
||||
FileNodeProperty::Id => {
|
||||
result.insert_unchecked(FileNodeProperty::Id, FileNodeValue::Id(id));
|
||||
}
|
||||
FileNodeProperty::Name => {
|
||||
result.insert_unchecked(FileNodeProperty::Name, file_node.name.to_string());
|
||||
}
|
||||
FileNodeProperty::ShareWith => {
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::ShareWith,
|
||||
JmapRights::share_with::<file_node::FileNode>(
|
||||
account_id,
|
||||
access_token,
|
||||
&file_node
|
||||
.acls
|
||||
.iter()
|
||||
.map(AclGrant::from)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
FileNodeProperty::MyRights => {
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::MyRights,
|
||||
if access_token.is_shared(account_id) {
|
||||
JmapRights::rights::<file_node::FileNode>(
|
||||
file_node.acls.effective_acl(access_token),
|
||||
)
|
||||
} else {
|
||||
JmapRights::all_rights::<file_node::FileNode>()
|
||||
},
|
||||
);
|
||||
}
|
||||
FileNodeProperty::ParentId => {
|
||||
let parent_id = file_node.parent_id.to_native();
|
||||
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::ParentId,
|
||||
if parent_id > 0 {
|
||||
Value::Element(FileNodeValue::Id((parent_id - 1).into()))
|
||||
} else {
|
||||
Value::Null
|
||||
},
|
||||
);
|
||||
}
|
||||
FileNodeProperty::BlobId => {
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::BlobId,
|
||||
if let Some(file) = file_node.file.as_ref() {
|
||||
Value::Element(FileNodeValue::BlobId(BlobId::new(
|
||||
BlobHash::from(&file.blob_hash),
|
||||
BlobClass::Linked {
|
||||
account_id,
|
||||
collection: Collection::FileNode.into(),
|
||||
document_id: id.document_id(),
|
||||
},
|
||||
)))
|
||||
} else {
|
||||
Value::Null
|
||||
},
|
||||
);
|
||||
}
|
||||
FileNodeProperty::Size => {
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::Size,
|
||||
if let Some(file) = file_node.file.as_ref() {
|
||||
Value::Number(file.size.to_native().into())
|
||||
} else {
|
||||
Value::Null
|
||||
},
|
||||
);
|
||||
}
|
||||
FileNodeProperty::Type => {
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::Type,
|
||||
if let Some(file) = file_node.file.as_ref() {
|
||||
Value::Str(
|
||||
file.media_type
|
||||
.as_ref()
|
||||
.map(|t| t.to_string())
|
||||
.unwrap_or_else(|| "application/octet-stream".to_string())
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
Value::Null
|
||||
},
|
||||
);
|
||||
}
|
||||
FileNodeProperty::Executable => {
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::Executable,
|
||||
if let Some(file) = file_node.file.as_ref() {
|
||||
Value::Bool(file.executable)
|
||||
} else {
|
||||
Value::Null
|
||||
},
|
||||
);
|
||||
}
|
||||
FileNodeProperty::Created => {
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::Created,
|
||||
Value::Element(FileNodeValue::Date(UTCDate::from_timestamp(
|
||||
file_node.created.to_native(),
|
||||
))),
|
||||
);
|
||||
}
|
||||
FileNodeProperty::Modified => {
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::Modified,
|
||||
Value::Element(FileNodeValue::Date(UTCDate::from_timestamp(
|
||||
file_node.modified.to_native(),
|
||||
))),
|
||||
);
|
||||
}
|
||||
FileNodeProperty::Accessed => {
|
||||
// TODO: needs serialization change (per-user accessed timestamp); returns now() as a placeholder
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::Accessed,
|
||||
Value::Element(FileNodeValue::Date(UTCDate::from_timestamp(
|
||||
now() as i64
|
||||
))),
|
||||
);
|
||||
}
|
||||
FileNodeProperty::Changed => {
|
||||
// TODO: needs serialization change (dedicated server-set changed timestamp); returns modified as a placeholder
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::Changed,
|
||||
Value::Element(FileNodeValue::Date(UTCDate::from_timestamp(
|
||||
file_node.modified.to_native(),
|
||||
))),
|
||||
);
|
||||
}
|
||||
FileNodeProperty::NodeType => {
|
||||
let node_type = if file_node.file.is_some() {
|
||||
FileNodeNodeType::File
|
||||
} else {
|
||||
FileNodeNodeType::Directory
|
||||
};
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::NodeType,
|
||||
Value::Str(node_type.as_str().into()),
|
||||
);
|
||||
}
|
||||
FileNodeProperty::Target => {
|
||||
result.insert_unchecked(FileNodeProperty::Target, Value::Null);
|
||||
}
|
||||
FileNodeProperty::Role => {
|
||||
result.insert_unchecked(FileNodeProperty::Role, Value::Null);
|
||||
}
|
||||
FileNodeProperty::IsSubscribed => {
|
||||
// TODO: needs serialization change (per-user subscription state); always true for now
|
||||
result.insert_unchecked(FileNodeProperty::IsSubscribed, Value::Bool(true));
|
||||
}
|
||||
property => {
|
||||
result.insert_unchecked(property.clone(), Value::Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
response.list.push(result.into());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod copy;
|
||||
pub mod get;
|
||||
pub mod query;
|
||||
pub mod set;
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use jmap_proto::{
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::file_node::{FileNode, FileNodeComparator, FileNodeFilter},
|
||||
request::MaybeInvalid,
|
||||
};
|
||||
use store::{
|
||||
ahash::AHashMap,
|
||||
roaring::RoaringBitmap,
|
||||
search::{SearchFilter, SearchQuery},
|
||||
write::SearchIndex,
|
||||
};
|
||||
use types::{acl::Acl, collection::SyncCollection};
|
||||
|
||||
pub trait FileNodeQuery: Sync + Send {
|
||||
fn file_node_query(
|
||||
&self,
|
||||
request: QueryRequest<FileNode>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
|
||||
}
|
||||
|
||||
impl FileNodeQuery for Server {
|
||||
async fn file_node_query(
|
||||
&self,
|
||||
mut request: QueryRequest<FileNode>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<QueryResponse> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let mut filters = Vec::with_capacity(request.filter.len());
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Property(cond) => match cond {
|
||||
FileNodeFilter::AncestorId(MaybeInvalid::Value(id)) => {
|
||||
if let Some(resource) =
|
||||
cache.container_resource_path_by_id(id.document_id())
|
||||
{
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.subtree(resource.path()).map(|r| r.document_id()),
|
||||
)))
|
||||
} else {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::new()));
|
||||
}
|
||||
}
|
||||
FileNodeFilter::DescendantId(MaybeInvalid::Value(id)) => {
|
||||
let mut ancestors = RoaringBitmap::new();
|
||||
let mut current = cache
|
||||
.any_resource_path_by_id(id.document_id())
|
||||
.and_then(|r| r.parent_id());
|
||||
while let Some(parent_id) = current {
|
||||
if !ancestors.insert(parent_id) {
|
||||
break;
|
||||
}
|
||||
current = cache
|
||||
.container_resource_by_id(parent_id)
|
||||
.and_then(|r| r.parent_id());
|
||||
}
|
||||
filters.push(SearchFilter::is_in_set(ancestors));
|
||||
}
|
||||
FileNodeFilter::ParentId(MaybeInvalid::Value(id)) => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.children_ids(id.document_id()),
|
||||
)));
|
||||
}
|
||||
FileNodeFilter::IsTopLevel(is_top_level) => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.resources.iter().filter_map(|r| {
|
||||
if is_top_level == r.parent_id().is_none() {
|
||||
Some(r.document_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
)));
|
||||
}
|
||||
FileNodeFilter::NodeType(node_type) => {
|
||||
let want_container = match node_type.as_str() {
|
||||
"directory" => Some(true),
|
||||
"file" => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
let set = match want_container {
|
||||
Some(is_container) => {
|
||||
RoaringBitmap::from_iter(cache.resources.iter().filter_map(|r| {
|
||||
if r.is_container() == is_container {
|
||||
Some(r.document_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}))
|
||||
}
|
||||
// TODO: support symlink nodeType once target storage exists
|
||||
None => RoaringBitmap::new(),
|
||||
};
|
||||
filters.push(SearchFilter::is_in_set(set));
|
||||
}
|
||||
FileNodeFilter::Name(name) => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.resources.iter().filter_map(|r| {
|
||||
if r.container_name().is_some_and(|n| n == name) {
|
||||
Some(r.document_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
)));
|
||||
}
|
||||
FileNodeFilter::NameMatch(name) => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.resources.iter().filter_map(|r| {
|
||||
if r.container_name().is_some_and(|n| name.matches(n)) {
|
||||
Some(r.document_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
)));
|
||||
}
|
||||
FileNodeFilter::MinSize(size) => {
|
||||
let size = size as u32;
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.resources.iter().filter_map(|r| {
|
||||
if r.size().is_some_and(|s| s >= size) {
|
||||
Some(r.document_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
)));
|
||||
}
|
||||
FileNodeFilter::MaxSize(size) => {
|
||||
let size = size as u32;
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.resources.iter().filter_map(|r| {
|
||||
if r.size().is_some_and(|s| s <= size) {
|
||||
Some(r.document_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
)));
|
||||
}
|
||||
// TODO: filters below require fetching archives or new indexes; ignore for now
|
||||
FileNodeFilter::Role(_)
|
||||
| FileNodeFilter::HasAnyRole(_)
|
||||
| FileNodeFilter::BlobId(_)
|
||||
| FileNodeFilter::IsExecutable(_)
|
||||
| FileNodeFilter::CreatedBefore(_)
|
||||
| FileNodeFilter::CreatedAfter(_)
|
||||
| FileNodeFilter::ModifiedBefore(_)
|
||||
| FileNodeFilter::ModifiedAfter(_)
|
||||
| FileNodeFilter::AccessedBefore(_)
|
||||
| FileNodeFilter::AccessedAfter(_)
|
||||
| FileNodeFilter::Type(_)
|
||||
| FileNodeFilter::TypeMatch(_)
|
||||
| FileNodeFilter::Text(_)
|
||||
| FileNodeFilter::Body(_)
|
||||
| FileNodeFilter::AncestorId(_)
|
||||
| FileNodeFilter::DescendantId(_)
|
||||
| FileNodeFilter::ParentId(_)
|
||||
| FileNodeFilter::_T(_) => {}
|
||||
},
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let results = SearchQuery::new(SearchIndex::InMemory)
|
||||
.with_filters(filters)
|
||||
.with_mask(if access_token.is_shared(account_id) {
|
||||
cache.shared_documents(access_token, [Acl::Read, Acl::ReadItems], true)
|
||||
} else {
|
||||
cache.resources.iter().map(|r| r.document_id).collect()
|
||||
})
|
||||
.filter()
|
||||
.into_bitmap();
|
||||
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len() as usize,
|
||||
self.core.jmap.query_max_results,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
);
|
||||
|
||||
// Only name, size and nodeType can be sorted from the cache.
|
||||
// TODO: created/modified/type/tree sorts require archive or hierarchy traversal
|
||||
let sortable = request
|
||||
.sort
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter(|c| {
|
||||
matches!(
|
||||
c.property,
|
||||
FileNodeComparator::Name
|
||||
| FileNodeComparator::Size
|
||||
| FileNodeComparator::NodeType
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if sortable.is_empty() {
|
||||
for document_id in results {
|
||||
if !response.add(0, document_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let by_id = cache
|
||||
.resources
|
||||
.iter()
|
||||
.map(|r| (r.document_id, r))
|
||||
.collect::<AHashMap<_, _>>();
|
||||
let mut ids = results.iter().collect::<Vec<_>>();
|
||||
ids.sort_unstable_by(|a, b| {
|
||||
for cmp in &sortable {
|
||||
let ra = by_id.get(a);
|
||||
let rb = by_id.get(b);
|
||||
let ordering = match cmp.property {
|
||||
FileNodeComparator::Name => ra
|
||||
.and_then(|r| r.container_name())
|
||||
.cmp(&rb.and_then(|r| r.container_name())),
|
||||
FileNodeComparator::Size => {
|
||||
ra.and_then(|r| r.size()).cmp(&rb.and_then(|r| r.size()))
|
||||
}
|
||||
FileNodeComparator::NodeType => {
|
||||
// Directories sort before files
|
||||
let a_dir = ra.map(|r| r.is_container()).unwrap_or(false);
|
||||
let b_dir = rb.map(|r| r.is_container()).unwrap_or(false);
|
||||
b_dir.cmp(&a_dir)
|
||||
}
|
||||
_ => std::cmp::Ordering::Equal,
|
||||
};
|
||||
let ordering = if cmp.is_ascending {
|
||||
ordering
|
||||
} else {
|
||||
ordering.reverse()
|
||||
};
|
||||
if ordering != std::cmp::Ordering::Equal {
|
||||
return ordering;
|
||||
}
|
||||
}
|
||||
a.cmp(b)
|
||||
});
|
||||
for document_id in ids {
|
||||
if !response.add(0, document_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user