JMAP for File Storage implementation (closes #2218)
This commit is contained in:
@@ -558,7 +558,7 @@ impl DavResourcePath<'_> {
|
||||
|
||||
#[inline(always)]
|
||||
pub fn size(&self) -> u32 {
|
||||
self.resource.size()
|
||||
self.resource.size().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,6 +692,20 @@ impl DavResource {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parent_id(&self) -> Option<u32> {
|
||||
match &self.data {
|
||||
DavResourceMetadata::File { parent_id, .. } => *parent_id,
|
||||
DavResourceMetadata::CalendarEvent { names, .. } => {
|
||||
names.first().map(|name| name.parent_id)
|
||||
}
|
||||
DavResourceMetadata::ContactCard { names } => names.first().map(|name| name.parent_id),
|
||||
DavResourceMetadata::CalendarScheduling { names } if names.is_empty() => {
|
||||
Some(SCHEDULE_INBOX_ID)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn child_names(&self) -> Option<&[DavName]> {
|
||||
match &self.data {
|
||||
DavResourceMetadata::CalendarEvent { names, .. } => Some(names.as_slice()),
|
||||
@@ -782,10 +796,10 @@ impl DavResource {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u32 {
|
||||
pub fn size(&self) -> Option<u32> {
|
||||
match &self.data {
|
||||
DavResourceMetadata::File { size, .. } => size.unwrap_or_default(),
|
||||
_ => 0,
|
||||
DavResourceMetadata::File { size, .. } => *size,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -263,6 +263,11 @@ impl Permission {
|
||||
Permission::JmapContactCardSet => "Create or update contact cards via JMAP",
|
||||
Permission::JmapContactCardCopy => "Copy contact cards to new locations via JMAP",
|
||||
Permission::JmapContactCardParse => "Parse contact cards via JMAP",
|
||||
Permission::JmapFileNodeGet => "Retrieve file nodes via JMAP",
|
||||
Permission::JmapFileNodeSet => "Create or update file nodes via JMAP",
|
||||
Permission::JmapFileNodeChanges => "Track file node changes via JMAP",
|
||||
Permission::JmapFileNodeQuery => "Search for file nodes matching criteria via JMAP",
|
||||
Permission::JmapFileNodeQueryChanges => "Track file node query changes via JMAP",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1475,6 +1475,11 @@ impl Permission {
|
||||
| Permission::JmapContactCardSet
|
||||
| Permission::JmapContactCardCopy
|
||||
| Permission::JmapContactCardParse
|
||||
| Permission::JmapFileNodeGet
|
||||
| Permission::JmapFileNodeSet
|
||||
| Permission::JmapFileNodeChanges
|
||||
| Permission::JmapFileNodeQuery
|
||||
| Permission::JmapFileNodeQueryChanges
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -391,6 +391,12 @@ pub enum Permission {
|
||||
JmapContactCardSet,
|
||||
JmapContactCardCopy,
|
||||
JmapContactCardParse,
|
||||
|
||||
JmapFileNodeGet,
|
||||
JmapFileNodeSet,
|
||||
JmapFileNodeChanges,
|
||||
JmapFileNodeQuery,
|
||||
JmapFileNodeQueryChanges,
|
||||
// WARNING: add new ids at the end (TODO: use static ids)
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,28 @@ impl DestroyArchive<Vec<u32>> {
|
||||
) -> trc::Result<()> {
|
||||
// Process deletions
|
||||
let mut batch = BatchBuilder::new();
|
||||
self.delete_batch(server, access_token, account_id, delete_path, &mut batch)
|
||||
.await?;
|
||||
// Write changes
|
||||
if !batch.is_empty() {
|
||||
server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_batch(
|
||||
self,
|
||||
server: &Server,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
delete_path: Option<String>,
|
||||
batch: &mut BatchBuilder,
|
||||
) -> trc::Result<()> {
|
||||
// Process deletions
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode);
|
||||
@@ -121,15 +143,10 @@ impl DestroyArchive<Vec<u32>> {
|
||||
}
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if !batch.is_empty() {
|
||||
if let Some(delete_path) = delete_path {
|
||||
batch.log_vanished_item(VanishedCollection::FileNode, delete_path);
|
||||
}
|
||||
server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if !batch.is_empty()
|
||||
&& let Some(delete_path) = delete_path
|
||||
{
|
||||
batch.log_vanished_item(VanishedCollection::FileNode, delete_path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -85,6 +85,8 @@ pub enum SetErrorType {
|
||||
ScriptIsActive,
|
||||
#[serde(rename = "addressBookHasContents")]
|
||||
AddressBookHasContents,
|
||||
#[serde(rename = "nodeHasChildren")]
|
||||
NodeHasChildren,
|
||||
}
|
||||
|
||||
impl SetErrorType {
|
||||
@@ -116,6 +118,7 @@ impl SetErrorType {
|
||||
SetErrorType::InvalidScript => "invalidScript",
|
||||
SetErrorType::ScriptIsActive => "scriptIsActive",
|
||||
SetErrorType::AddressBookHasContents => "addressBookHasContents",
|
||||
SetErrorType::NodeHasChildren => "nodeHasChildren",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,6 +196,10 @@ impl<T: Property> SetError<T> {
|
||||
Self::new(SetErrorType::AddressBookHasContents)
|
||||
.with_description("Address book is not empty.")
|
||||
}
|
||||
|
||||
pub fn node_has_children() -> Self {
|
||||
Self::new(SetErrorType::NodeHasChildren).with_description("File node has children.")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Property> From<T> for InvalidProperty<T> {
|
||||
|
||||
590
crates/jmap-proto/src/object/file_node.rs
Normal file
590
crates/jmap-proto/src/object/file_node.rs
Normal file
@@ -0,0 +1,590 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{
|
||||
AnyId, JmapObject, JmapObjectId, JmapRight, JmapSharedObject, MaybeReference, parse_ref,
|
||||
},
|
||||
request::{MaybeInvalid, deserialize::DeserializeArguments},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::{acl::Acl, blob::BlobId, id::Id};
|
||||
use utils::glob::GlobPattern;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FileNode;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum FileNodeProperty {
|
||||
Id,
|
||||
ParentId,
|
||||
BlobId,
|
||||
Size,
|
||||
Name,
|
||||
Type,
|
||||
Created,
|
||||
Modified,
|
||||
Accessed,
|
||||
Executable,
|
||||
MyRights,
|
||||
ShareWith,
|
||||
|
||||
// Other
|
||||
IdValue(Id),
|
||||
Rights(FileNodeRight),
|
||||
Pointer(JsonPointer<FileNodeProperty>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum FileNodeRight {
|
||||
MayRead,
|
||||
MayWrite,
|
||||
MayShare,
|
||||
MayDelete,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum FileNodeValue {
|
||||
Id(Id),
|
||||
Date(UTCDate),
|
||||
BlobId(BlobId),
|
||||
IdReference(String),
|
||||
}
|
||||
|
||||
impl Property for FileNodeProperty {
|
||||
fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
let allow_patch = key.is_none();
|
||||
if let Some(Key::Property(key)) = key {
|
||||
match key.patch_or_prop() {
|
||||
FileNodeProperty::ShareWith => {
|
||||
Id::from_str(value).ok().map(FileNodeProperty::IdValue)
|
||||
}
|
||||
_ => FileNodeProperty::parse(value, allow_patch),
|
||||
}
|
||||
} else {
|
||||
FileNodeProperty::parse(value, allow_patch)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
FileNodeProperty::Id => "id",
|
||||
FileNodeProperty::ParentId => "parentId",
|
||||
FileNodeProperty::BlobId => "blobId",
|
||||
FileNodeProperty::Size => "size",
|
||||
FileNodeProperty::Name => "name",
|
||||
FileNodeProperty::Type => "type",
|
||||
FileNodeProperty::Created => "created",
|
||||
FileNodeProperty::Modified => "modified",
|
||||
FileNodeProperty::Accessed => "accessed",
|
||||
FileNodeProperty::Executable => "executable",
|
||||
FileNodeProperty::MyRights => "myRights",
|
||||
FileNodeProperty::ShareWith => "shareWith",
|
||||
FileNodeProperty::Rights(file_right) => file_right.as_str(),
|
||||
FileNodeProperty::Pointer(json_pointer) => return json_pointer.to_string().into(),
|
||||
FileNodeProperty::IdValue(id) => return id.to_string().into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FileNodeRight {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
FileNodeRight::MayRead => "mayRead",
|
||||
FileNodeRight::MayWrite => "mayWrite",
|
||||
FileNodeRight::MayShare => "mayShare",
|
||||
FileNodeRight::MayDelete => "mayDelete",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for FileNodeValue {
|
||||
type Property = FileNodeProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop.patch_or_prop() {
|
||||
FileNodeProperty::Id | FileNodeProperty::ParentId => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(FileNodeValue::Id(v)),
|
||||
MaybeReference::Reference(v) => Some(FileNodeValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
FileNodeProperty::BlobId => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(FileNodeValue::BlobId(v)),
|
||||
MaybeReference::Reference(v) => Some(FileNodeValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
FileNodeProperty::Created
|
||||
| FileNodeProperty::Modified
|
||||
| FileNodeProperty::Accessed => {
|
||||
UTCDate::from_str(value).ok().map(FileNodeValue::Date)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
FileNodeValue::Id(id) => id.to_string().into(),
|
||||
FileNodeValue::Date(utcdate) => utcdate.to_string().into(),
|
||||
FileNodeValue::BlobId(blob_id) => blob_id.to_string().into(),
|
||||
FileNodeValue::IdReference(r) => format!("#{r}").into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileNodeProperty {
|
||||
fn parse(value: &str, allow_patch: bool) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => FileNodeProperty::Id,
|
||||
b"parentId" => FileNodeProperty::ParentId,
|
||||
b"blobId" => FileNodeProperty::BlobId,
|
||||
b"size" => FileNodeProperty::Size,
|
||||
b"name" => FileNodeProperty::Name,
|
||||
b"type" => FileNodeProperty::Type,
|
||||
b"created" => FileNodeProperty::Created,
|
||||
b"modified" => FileNodeProperty::Modified,
|
||||
b"accessed" => FileNodeProperty::Accessed,
|
||||
b"executable" => FileNodeProperty::Executable,
|
||||
b"myRights" => FileNodeProperty::MyRights,
|
||||
b"shareWith" => FileNodeProperty::ShareWith,
|
||||
b"mayRead" => FileNodeProperty::Rights(FileNodeRight::MayRead),
|
||||
b"mayWrite" => FileNodeProperty::Rights(FileNodeRight::MayWrite),
|
||||
b"mayShare" => FileNodeProperty::Rights(FileNodeRight::MayShare),
|
||||
b"mayDelete" => FileNodeProperty::Rights(FileNodeRight::MayDelete)
|
||||
)
|
||||
.or_else(|| {
|
||||
if allow_patch && value.contains('/') {
|
||||
FileNodeProperty::Pointer(JsonPointer::parse(value)).into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn patch_or_prop(&self) -> &FileNodeProperty {
|
||||
if let FileNodeProperty::Pointer(ptr) = self
|
||||
&& let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last()
|
||||
{
|
||||
prop
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FileNodeSetArguments {
|
||||
pub on_destroy_remove_children: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'x> DeserializeArguments<'x> for FileNodeSetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'x>,
|
||||
{
|
||||
if key == "onDestroyRemoveChildren" {
|
||||
self.on_destroy_remove_children = map.next_value()?;
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FileNodeQueryArguments {
|
||||
pub depth: Option<u32>,
|
||||
}
|
||||
|
||||
impl<'x> DeserializeArguments<'x> for FileNodeQueryArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'x>,
|
||||
{
|
||||
if key == "depth" {
|
||||
self.depth = map.next_value()?;
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for FileNodeProperty {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_cow().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FileNodeProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
FileNodeProperty::parse(s, false).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for FileNode {
|
||||
type Property = FileNodeProperty;
|
||||
|
||||
type Element = FileNodeValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = FileNodeFilter;
|
||||
|
||||
type Comparator = FileNodeComparator;
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = FileNodeSetArguments;
|
||||
|
||||
type QueryArguments = FileNodeQueryArguments;
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = FileNodeProperty::Id;
|
||||
}
|
||||
|
||||
impl JmapSharedObject for FileNode {
|
||||
type Right = FileNodeRight;
|
||||
|
||||
const SHARE_WITH_PROPERTY: Self::Property = FileNodeProperty::ShareWith;
|
||||
}
|
||||
|
||||
impl From<Id> for FileNodeProperty {
|
||||
fn from(id: Id) -> Self {
|
||||
FileNodeProperty::IdValue(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapRight for FileNodeRight {
|
||||
fn from_acl(acl: Acl) -> &'static [Self] {
|
||||
match acl {
|
||||
Acl::ReadItems => &[FileNodeRight::MayRead],
|
||||
Acl::RemoveItems => &[FileNodeRight::MayDelete],
|
||||
Acl::ModifyItems => &[FileNodeRight::MayWrite],
|
||||
Acl::Delete => &[FileNodeRight::MayDelete],
|
||||
Acl::Administer => &[FileNodeRight::MayShare],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn to_acl(&self) -> &'static [Acl] {
|
||||
match self {
|
||||
FileNodeRight::MayDelete => &[Acl::Delete, Acl::RemoveItems],
|
||||
FileNodeRight::MayShare => &[Acl::Administer],
|
||||
FileNodeRight::MayRead => &[Acl::Read, Acl::ReadItems],
|
||||
FileNodeRight::MayWrite => &[Acl::Modify, Acl::AddItems, Acl::ModifyItems],
|
||||
}
|
||||
}
|
||||
|
||||
fn all_rights() -> &'static [Self] {
|
||||
&[
|
||||
FileNodeRight::MayRead,
|
||||
FileNodeRight::MayWrite,
|
||||
FileNodeRight::MayDelete,
|
||||
FileNodeRight::MayShare,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileNodeRight> for FileNodeProperty {
|
||||
fn from(right: FileNodeRight) -> Self {
|
||||
FileNodeProperty::Rights(right)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum FileNodeFilter {
|
||||
HasParentId(bool),
|
||||
ParentId(MaybeInvalid<Id>),
|
||||
AncestorId(MaybeInvalid<Id>),
|
||||
HasType(bool),
|
||||
BlobId(MaybeInvalid<BlobId>),
|
||||
IsExecutable(bool),
|
||||
CreatedBefore(UTCDate),
|
||||
CreatedAfter(UTCDate),
|
||||
ModifiedBefore(UTCDate),
|
||||
ModifiedAfter(UTCDate),
|
||||
AccessedBefore(UTCDate),
|
||||
AccessedAfter(UTCDate),
|
||||
MinSize(u64),
|
||||
MaxSize(u64),
|
||||
Name(String),
|
||||
NameMatch(GlobPattern),
|
||||
Type(String),
|
||||
TypeMatch(GlobPattern),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum FileNodeComparator {
|
||||
Name,
|
||||
Size,
|
||||
Created,
|
||||
Modified,
|
||||
Type,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for FileNodeFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"hasParentId" => {
|
||||
*self = FileNodeFilter::HasParentId(map.next_value()?);
|
||||
},
|
||||
b"parentId" => {
|
||||
*self = FileNodeFilter::ParentId(map.next_value()?);
|
||||
},
|
||||
b"ancestorId" => {
|
||||
*self = FileNodeFilter::AncestorId(map.next_value()?);
|
||||
},
|
||||
b"hasType" => {
|
||||
*self = FileNodeFilter::HasType(map.next_value()?);
|
||||
},
|
||||
b"blobId" => {
|
||||
*self = FileNodeFilter::BlobId(map.next_value()?);
|
||||
},
|
||||
b"isExecutable" => {
|
||||
*self = FileNodeFilter::IsExecutable(map.next_value()?);
|
||||
},
|
||||
b"createdBefore" => {
|
||||
*self = FileNodeFilter::CreatedBefore(map.next_value()?);
|
||||
},
|
||||
b"createdAfter" => {
|
||||
*self = FileNodeFilter::CreatedAfter(map.next_value()?);
|
||||
},
|
||||
b"modifiedBefore" => {
|
||||
*self = FileNodeFilter::ModifiedBefore(map.next_value()?);
|
||||
},
|
||||
b"modifiedAfter" => {
|
||||
*self = FileNodeFilter::ModifiedAfter(map.next_value()?);
|
||||
},
|
||||
b"accessedBefore" => {
|
||||
*self = FileNodeFilter::AccessedBefore(map.next_value()?);
|
||||
},
|
||||
b"accessedAfter" => {
|
||||
*self = FileNodeFilter::AccessedAfter(map.next_value()?);
|
||||
},
|
||||
b"minSize" => {
|
||||
*self = FileNodeFilter::MinSize(map.next_value()?);
|
||||
},
|
||||
b"maxSize" => {
|
||||
*self = FileNodeFilter::MaxSize(map.next_value()?);
|
||||
},
|
||||
b"name" => {
|
||||
*self = FileNodeFilter::Name(map.next_value()?);
|
||||
},
|
||||
b"nameMatch" => {
|
||||
*self = FileNodeFilter::NameMatch(map.next_value()?);
|
||||
},
|
||||
b"type" => {
|
||||
*self = FileNodeFilter::Type(map.next_value()?);
|
||||
},
|
||||
b"typeMatch" => {
|
||||
*self = FileNodeFilter::TypeMatch(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
*self = FileNodeFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for FileNodeComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"name" => {
|
||||
*self = FileNodeComparator::Name;
|
||||
},
|
||||
b"size" => {
|
||||
*self = FileNodeComparator::Size;
|
||||
},
|
||||
b"created" => {
|
||||
*self = FileNodeComparator::Created;
|
||||
},
|
||||
b"modified" => {
|
||||
*self = FileNodeComparator::Modified;
|
||||
},
|
||||
b"type" => {
|
||||
*self = FileNodeComparator::Type;
|
||||
},
|
||||
_ => {
|
||||
*self = FileNodeComparator::_T(key.to_string());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileNodeFilter {
|
||||
fn default() -> Self {
|
||||
FileNodeFilter::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileNodeComparator {
|
||||
fn default() -> Self {
|
||||
FileNodeComparator::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for FileNodeValue {
|
||||
fn from(id: Id) -> Self {
|
||||
FileNodeValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for FileNodeValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
FileNodeValue::Id(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
FileNodeValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
FileNodeValue::BlobId(blob_id) => Some(AnyId::BlobId(blob_id.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
if let FileNodeValue::IdReference(r) = self {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for FileNodeValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
AnyId::Id(id) => Ok(FileNodeValue::Id(id)),
|
||||
AnyId::BlobId(blob_id) => Ok(FileNodeValue::BlobId(blob_id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileNodeFilter {
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
FileNodeFilter::HasParentId(_) => "hasParentId",
|
||||
FileNodeFilter::ParentId(_) => "parentId",
|
||||
FileNodeFilter::AncestorId(_) => "ancestorId",
|
||||
FileNodeFilter::HasType(_) => "hasType",
|
||||
FileNodeFilter::BlobId(_) => "blobId",
|
||||
FileNodeFilter::IsExecutable(_) => "isExecutable",
|
||||
FileNodeFilter::CreatedBefore(_) => "createdBefore",
|
||||
FileNodeFilter::CreatedAfter(_) => "createdAfter",
|
||||
FileNodeFilter::ModifiedBefore(_) => "modifiedBefore",
|
||||
FileNodeFilter::ModifiedAfter(_) => "modifiedAfter",
|
||||
FileNodeFilter::AccessedBefore(_) => "accessedBefore",
|
||||
FileNodeFilter::AccessedAfter(_) => "accessedAfter",
|
||||
FileNodeFilter::MinSize(_) => "minSize",
|
||||
FileNodeFilter::MaxSize(_) => "maxSize",
|
||||
FileNodeFilter::Name(_) => "name",
|
||||
FileNodeFilter::NameMatch(_) => "nameMatch",
|
||||
FileNodeFilter::Type(_) => "type",
|
||||
FileNodeFilter::TypeMatch(_) => "typeMatch",
|
||||
FileNodeFilter::_T(s) => return s.into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FileNodeComparator {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
FileNodeComparator::Name => "name",
|
||||
FileNodeComparator::Size => "size",
|
||||
FileNodeComparator::Created => "created",
|
||||
FileNodeComparator::Modified => "modified",
|
||||
FileNodeComparator::Type => "type",
|
||||
FileNodeComparator::_T(s) => s.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
FileNodeComparator::Name => "name",
|
||||
FileNodeComparator::Size => "size",
|
||||
FileNodeComparator::Created => "created",
|
||||
FileNodeComparator::Modified => "modified",
|
||||
FileNodeComparator::Type => "type",
|
||||
FileNodeComparator::_T(s) => return s.into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for FileNodeComparator {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<FileNodeProperty> for Id {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: FileNodeProperty) -> Result<Self, Self::Error> {
|
||||
if let FileNodeProperty::IdValue(id) = value {
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<FileNodeProperty> for FileNodeRight {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: FileNodeProperty) -> Result<Self, Self::Error> {
|
||||
if let FileNodeProperty::Rights(right) = value {
|
||||
Ok(right)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ pub mod blob;
|
||||
pub mod contact;
|
||||
pub mod email;
|
||||
pub mod email_submission;
|
||||
pub mod file_node;
|
||||
pub mod identity;
|
||||
pub mod mailbox;
|
||||
pub mod principal;
|
||||
|
||||
@@ -63,6 +63,9 @@ impl Response<'_> {
|
||||
GetResponseMethod::ContactCard(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::FileNode(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
},
|
||||
ResponseMethod::Changes(response) => match response {
|
||||
ChangesResponseMethod::Email(response) => {
|
||||
@@ -89,6 +92,9 @@ impl Response<'_> {
|
||||
ChangesResponseMethod::ContactCard(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::FileNode(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
},
|
||||
ResponseMethod::Query(response) => response.eval_jptr(path, &mut results),
|
||||
ResponseMethod::QueryChanges(response) => {
|
||||
|
||||
@@ -44,6 +44,7 @@ impl Response<'_> {
|
||||
GetRequestMethod::Blob(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::AddressBook(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::ContactCard(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::FileNode(request) => request.resolve_references(self)?,
|
||||
},
|
||||
RequestMethod::Set(request) => match request {
|
||||
SetRequestMethod::Email(request) => request.resolve_references(self)?,
|
||||
@@ -55,6 +56,7 @@ impl Response<'_> {
|
||||
SetRequestMethod::VacationResponse(request) => request.resolve_references(self)?,
|
||||
SetRequestMethod::AddressBook(request) => request.resolve_references(self)?,
|
||||
SetRequestMethod::ContactCard(request) => request.resolve_references(self)?,
|
||||
SetRequestMethod::FileNode(request) => request.resolve_references(self)?,
|
||||
},
|
||||
RequestMethod::Copy(request) => match request {
|
||||
CopyRequestMethod::Email(request) => request.resolve_references(self)?,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::response::serialize::serialize_hex;
|
||||
use crate::{object::file_node::FileNodeComparator, response::serialize::serialize_hex};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use types::{id::Id, type_state::DataType};
|
||||
use utils::map::vec_map::VecMap;
|
||||
@@ -74,6 +74,8 @@ pub enum Capability {
|
||||
Principals = 1 << 10,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:principals:owner"))]
|
||||
PrincipalsOwner = 1 << 11,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:filenode"))]
|
||||
FileNode = 1 << 12,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
@@ -94,6 +96,7 @@ pub enum Capabilities {
|
||||
Contacts(ContactsCapabilities),
|
||||
Principals(PrincipalsCapabilities),
|
||||
PrincipalsOwner(PrincipalsOwnerCapabilities),
|
||||
FileNode(FileNodeCapabilities),
|
||||
Empty(EmptyCapabilities),
|
||||
}
|
||||
|
||||
@@ -208,6 +211,18 @@ pub struct PrincipalsOwnerCapabilities {
|
||||
pub principal_id: Id,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct FileNodeCapabilities {
|
||||
#[serde(rename(serialize = "maxFileNodeDepth"))]
|
||||
pub max_file_node_depth: Option<usize>,
|
||||
#[serde(rename(serialize = "maxSizeFileNodeName"))]
|
||||
pub max_size_file_node_name: usize,
|
||||
#[serde(rename(serialize = "fileNodeQuerySortOptions"))]
|
||||
pub file_node_query_sort_options: Vec<FileNodeComparator>,
|
||||
#[serde(rename(serialize = "mayCreateTopLevelFileNode"))]
|
||||
pub may_create_top_level_file_node: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct EmptyCapabilities {}
|
||||
|
||||
@@ -363,7 +378,8 @@ impl Capability {
|
||||
"urn:ietf:params:jmap:blob" => Capability::Blob,
|
||||
"urn:ietf:params:jmap:quota" => Capability::Quota,
|
||||
"urn:ietf:params:jmap:principals" => Capability::Principals,
|
||||
"urn:ietf:params:jmap:principals:owner" => Capability::PrincipalsOwner
|
||||
"urn:ietf:params:jmap:principals:owner" => Capability::PrincipalsOwner,
|
||||
"urn:ietf:params:jmap:filenode" => Capability::FileNode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub enum MethodObject {
|
||||
Quota,
|
||||
AddressBook,
|
||||
ContactCard,
|
||||
FileNode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -136,6 +137,12 @@ impl MethodName {
|
||||
(MethodFunction::Copy, MethodObject::ContactCard) => "ContactCard/copy",
|
||||
(MethodFunction::Parse, MethodObject::ContactCard) => "ContactCard/parse",
|
||||
|
||||
(MethodFunction::Get, MethodObject::FileNode) => "FileNode/get",
|
||||
(MethodFunction::Changes, MethodObject::FileNode) => "FileNode/changes",
|
||||
(MethodFunction::Query, MethodObject::FileNode) => "FileNode/query",
|
||||
(MethodFunction::QueryChanges, MethodObject::FileNode) => "FileNode/queryChanges",
|
||||
(MethodFunction::Set, MethodObject::FileNode) => "FileNode/set",
|
||||
|
||||
(MethodFunction::Echo, MethodObject::Core) => "Core/echo",
|
||||
_ => "error",
|
||||
}
|
||||
@@ -210,6 +217,12 @@ impl MethodName {
|
||||
"ContactCard/copy" => (MethodObject::ContactCard, MethodFunction::Copy),
|
||||
"ContactCard/parse" => (MethodObject::ContactCard, MethodFunction::Parse),
|
||||
|
||||
"FileNode/get" => (MethodObject::FileNode, MethodFunction::Get),
|
||||
"FileNode/changes" => (MethodObject::FileNode, MethodFunction::Changes),
|
||||
"FileNode/query" => (MethodObject::FileNode, MethodFunction::Query),
|
||||
"FileNode/queryChanges" => (MethodObject::FileNode, MethodFunction::QueryChanges),
|
||||
"FileNode/set" => (MethodObject::FileNode, MethodFunction::Set),
|
||||
|
||||
"Core/echo" => (MethodObject::Core, MethodFunction::Echo),
|
||||
|
||||
).map(|(obj, fnc)| MethodName { obj, fnc })
|
||||
@@ -235,6 +248,7 @@ impl Display for MethodObject {
|
||||
MethodObject::Quota => "Quota",
|
||||
MethodObject::AddressBook => "AddressBook",
|
||||
MethodObject::ContactCard => "ContactCard",
|
||||
MethodObject::FileNode => "FileNode",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ use crate::{
|
||||
},
|
||||
object::{
|
||||
AnyId, addressbook::AddressBook, blob::Blob, contact::ContactCard, email::Email,
|
||||
email_submission::EmailSubmission, identity::Identity, mailbox::Mailbox,
|
||||
principal::Principal, push_subscription::PushSubscription, quota::Quota, sieve::Sieve,
|
||||
thread::Thread, vacation_response::VacationResponse,
|
||||
email_submission::EmailSubmission, file_node::FileNode, identity::Identity,
|
||||
mailbox::Mailbox, principal::Principal, push_subscription::PushSubscription, quota::Quota,
|
||||
sieve::Sieve, thread::Thread, vacation_response::VacationResponse,
|
||||
},
|
||||
request::{capability::CapabilityIds, reference::MaybeIdReference},
|
||||
};
|
||||
@@ -86,6 +86,7 @@ pub enum GetRequestMethod {
|
||||
Blob(GetRequest<Blob>),
|
||||
AddressBook(GetRequest<AddressBook>),
|
||||
ContactCard(GetRequest<ContactCard>),
|
||||
FileNode(GetRequest<FileNode>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -99,6 +100,7 @@ pub enum SetRequestMethod<'x> {
|
||||
VacationResponse(SetRequest<'x, VacationResponse>),
|
||||
AddressBook(SetRequest<'x, AddressBook>),
|
||||
ContactCard(SetRequest<'x, ContactCard>),
|
||||
FileNode(SetRequest<'x, FileNode>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -117,6 +119,7 @@ pub enum QueryRequestMethod {
|
||||
Principal(QueryRequest<Principal>),
|
||||
Quota(QueryRequest<Quota>),
|
||||
ContactCard(QueryRequest<ContactCard>),
|
||||
FileNode(QueryRequest<FileNode>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -128,6 +131,7 @@ pub enum QueryChangesRequestMethod {
|
||||
Principal(QueryChangesRequest<Principal>),
|
||||
Quota(QueryChangesRequest<Quota>),
|
||||
ContactCard(QueryChangesRequest<ContactCard>),
|
||||
FileNode(QueryChangesRequest<FileNode>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -195,6 +195,13 @@ impl<'de> Visitor<'de> for CallVisitor {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::FileNode) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::FileNode(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::SearchSnippet) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::SearchSnippet(value),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
@@ -265,6 +272,13 @@ impl<'de> Visitor<'de> for CallVisitor {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::FileNode) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::FileNode(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::Email) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Email(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
@@ -314,6 +328,13 @@ impl<'de> Visitor<'de> for CallVisitor {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::FileNode) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::FileNode(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::QueryChanges, MethodObject::Email) => match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::QueryChanges(QueryChangesRequestMethod::Email(value))
|
||||
@@ -379,6 +400,15 @@ impl<'de> Visitor<'de> for CallVisitor {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::QueryChanges, MethodObject::FileNode) => match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::QueryChanges(QueryChangesRequestMethod::FileNode(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Changes, _) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Changes(value),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
|
||||
@@ -26,9 +26,9 @@ use crate::{
|
||||
},
|
||||
object::{
|
||||
AnyId, addressbook::AddressBook, blob::Blob, contact::ContactCard, email::Email,
|
||||
email_submission::EmailSubmission, identity::Identity, mailbox::Mailbox,
|
||||
principal::Principal, push_subscription::PushSubscription, quota::Quota, sieve::Sieve,
|
||||
thread::Thread, vacation_response::VacationResponse,
|
||||
email_submission::EmailSubmission, file_node::FileNode, identity::Identity,
|
||||
mailbox::Mailbox, principal::Principal, push_subscription::PushSubscription, quota::Quota,
|
||||
sieve::Sieve, thread::Thread, vacation_response::VacationResponse,
|
||||
},
|
||||
request::{Call, method::MethodName},
|
||||
};
|
||||
@@ -70,6 +70,7 @@ pub enum GetResponseMethod {
|
||||
Blob(GetResponse<Blob>),
|
||||
AddressBook(GetResponse<AddressBook>),
|
||||
ContactCard(GetResponse<ContactCard>),
|
||||
FileNode(GetResponse<FileNode>),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
@@ -84,6 +85,7 @@ pub enum SetResponseMethod {
|
||||
VacationResponse(SetResponse<VacationResponse>),
|
||||
AddressBook(SetResponse<AddressBook>),
|
||||
ContactCard(SetResponse<ContactCard>),
|
||||
FileNode(SetResponse<FileNode>),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
@@ -97,6 +99,7 @@ pub enum ChangesResponseMethod {
|
||||
Quota(ChangesResponse<Quota>),
|
||||
AddressBook(ChangesResponse<AddressBook>),
|
||||
ContactCard(ChangesResponse<ContactCard>),
|
||||
FileNode(ChangesResponse<FileNode>),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
@@ -435,3 +438,21 @@ impl<'x> From<MethodErrorWrapper> for ResponseMethod<'x> {
|
||||
ResponseMethod::Error(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GetResponse<FileNode>> for ResponseMethod<'_> {
|
||||
fn from(response: GetResponse<FileNode>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::FileNode(response))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SetResponse<FileNode>> for ResponseMethod<'_> {
|
||||
fn from(response: SetResponse<FileNode>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::FileNode(response))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChangesResponse<FileNode>> for ResponseMethod<'_> {
|
||||
fn from(response: ChangesResponse<FileNode>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::FileNode(response))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ impl AddressBookGet for Server {
|
||||
}
|
||||
AddressBookProperty::MyRights => {
|
||||
result.insert_unchecked(
|
||||
AddressBookProperty::IsDefault,
|
||||
AddressBookProperty::MyRights,
|
||||
if access_token.is_shared(account_id) {
|
||||
JmapRights::rights::<addressbook::AddressBook>(
|
||||
address_book.acls.effective_acl(access_token),
|
||||
|
||||
@@ -69,6 +69,7 @@ impl JmapAuthorization for AccessToken {
|
||||
GetRequestMethod::Blob(_) => Permission::JmapBlobGet,
|
||||
GetRequestMethod::AddressBook(_) => Permission::JmapAddressBookGet,
|
||||
GetRequestMethod::ContactCard(_) => Permission::JmapContactCardGet,
|
||||
GetRequestMethod::FileNode(_) => Permission::JmapFileNodeGet,
|
||||
},
|
||||
RequestMethod::Set(m) => match &m {
|
||||
SetRequestMethod::Email(_) => Permission::JmapEmailSet,
|
||||
@@ -80,6 +81,7 @@ impl JmapAuthorization for AccessToken {
|
||||
SetRequestMethod::VacationResponse(_) => Permission::JmapVacationResponseSet,
|
||||
SetRequestMethod::AddressBook(_) => Permission::JmapAddressBookSet,
|
||||
SetRequestMethod::ContactCard(_) => Permission::JmapContactCardSet,
|
||||
SetRequestMethod::FileNode(_) => Permission::JmapFileNodeSet,
|
||||
},
|
||||
RequestMethod::Changes(_) => match object {
|
||||
MethodObject::Email => Permission::JmapEmailChanges,
|
||||
@@ -89,6 +91,7 @@ impl JmapAuthorization for AccessToken {
|
||||
MethodObject::EmailSubmission => Permission::JmapEmailSubmissionChanges,
|
||||
MethodObject::Quota => Permission::JmapQuotaChanges,
|
||||
MethodObject::ContactCard => Permission::JmapContactCardChanges,
|
||||
MethodObject::FileNode => Permission::JmapFileNodeChanges,
|
||||
MethodObject::Core
|
||||
| MethodObject::Blob
|
||||
| MethodObject::PushSubscription
|
||||
@@ -120,6 +123,7 @@ impl JmapAuthorization for AccessToken {
|
||||
QueryChangesRequestMethod::ContactCard(_) => {
|
||||
Permission::JmapContactCardQueryChanges
|
||||
}
|
||||
QueryChangesRequestMethod::FileNode(_) => Permission::JmapFileNodeQueryChanges,
|
||||
},
|
||||
RequestMethod::Query(m) => match m {
|
||||
QueryRequestMethod::Email(_) => Permission::JmapEmailQuery,
|
||||
@@ -129,6 +133,7 @@ impl JmapAuthorization for AccessToken {
|
||||
QueryRequestMethod::Principal(_) => Permission::JmapPrincipalQuery,
|
||||
QueryRequestMethod::Quota(_) => Permission::JmapQuotaQuery,
|
||||
QueryRequestMethod::ContactCard(_) => Permission::JmapContactCardQuery,
|
||||
QueryRequestMethod::FileNode(_) => Permission::JmapFileNodeQuery,
|
||||
},
|
||||
RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippet,
|
||||
RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate,
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::{
|
||||
copy::JmapEmailCopy, get::EmailGet, import::EmailImport, parse::EmailParse,
|
||||
query::EmailQuery, set::EmailSet, snippet::EmailSearchSnippet,
|
||||
},
|
||||
file::{get::FileNodeGet, query::FileNodeQuery, set::FileNodeSet},
|
||||
identity::{get::IdentityGet, set::IdentitySet},
|
||||
mailbox::{get::MailboxGet, query::MailboxQuery, set::MailboxSet},
|
||||
principal::{get::PrincipalGet, query::PrincipalQuery},
|
||||
@@ -135,6 +136,9 @@ impl RequestHandler for Server {
|
||||
SetResponseMethod::ContactCard(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::FileNode(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
}
|
||||
}
|
||||
ResponseMethod::ImportEmail(import_response) => {
|
||||
@@ -269,6 +273,12 @@ impl RequestHandler for Server {
|
||||
|
||||
self.contact_card_get(req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::FileNode(mut req) => {
|
||||
set_account_id_if_missing(&mut req.account_id, access_token);
|
||||
access_token.assert_has_access(req.account_id, Collection::FileNode)?;
|
||||
|
||||
self.file_node_get(req, access_token).await?.into()
|
||||
}
|
||||
},
|
||||
RequestMethod::Query(req) => match req {
|
||||
QueryRequestMethod::Email(mut req) => {
|
||||
@@ -311,6 +321,12 @@ impl RequestHandler for Server {
|
||||
|
||||
self.contact_card_query(req, access_token).await?.into()
|
||||
}
|
||||
QueryRequestMethod::FileNode(mut req) => {
|
||||
set_account_id_if_missing(&mut req.account_id, access_token);
|
||||
access_token.assert_has_access(req.account_id, Collection::FileNode)?;
|
||||
|
||||
self.file_node_query(req, access_token).await?.into()
|
||||
}
|
||||
},
|
||||
RequestMethod::Set(req) => match req {
|
||||
SetRequestMethod::Email(mut req) => {
|
||||
@@ -373,6 +389,12 @@ impl RequestHandler for Server {
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
SetRequestMethod::FileNode(mut req) => {
|
||||
set_account_id_if_missing(&mut req.account_id, access_token);
|
||||
access_token.assert_has_access(req.account_id, Collection::FileNode)?;
|
||||
|
||||
self.file_node_set(req, access_token, session).await?.into()
|
||||
}
|
||||
},
|
||||
RequestMethod::Changes(mut req) => {
|
||||
set_account_id_if_missing(&mut req.account_id, access_token);
|
||||
|
||||
@@ -65,6 +65,16 @@ impl ChangesLookup for Server {
|
||||
|
||||
(SyncCollection::EmailSubmission, false)
|
||||
}
|
||||
MethodObject::ContactCard => {
|
||||
access_token.assert_has_access(request.account_id, Collection::AddressBook)?;
|
||||
|
||||
(SyncCollection::AddressBook, false)
|
||||
}
|
||||
MethodObject::FileNode => {
|
||||
access_token.assert_has_access(request.account_id, Collection::FileNode)?;
|
||||
|
||||
(SyncCollection::FileNode, true)
|
||||
}
|
||||
_ => {
|
||||
access_token.assert_is_member(request.account_id)?;
|
||||
|
||||
@@ -233,6 +243,9 @@ impl IntermediateChangesResponse {
|
||||
MethodObject::ContactCard => {
|
||||
ChangesResponseMethod::ContactCard(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::FileNode => {
|
||||
ChangesResponseMethod::FileNode(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::Core
|
||||
| MethodObject::Blob
|
||||
| MethodObject::PushSubscription
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
use super::get::ChangesLookup;
|
||||
use crate::{
|
||||
api::request::set_account_id_if_missing, contact::query::ContactCardQuery,
|
||||
email::query::EmailQuery, mailbox::query::MailboxQuery, sieve::query::SieveScriptQuery,
|
||||
submission::query::EmailSubmissionQuery,
|
||||
email::query::EmailQuery, file::query::FileNodeQuery, mailbox::query::MailboxQuery,
|
||||
sieve::query::SieveScriptQuery, submission::query::EmailSubmissionQuery,
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use jmap_proto::{
|
||||
@@ -161,6 +161,28 @@ impl QueryChanges for Server {
|
||||
.contact_card_query(request.into(), access_token)
|
||||
.await?;
|
||||
}
|
||||
QueryChangesRequestMethod::FileNode(mut request) => {
|
||||
// Query changes
|
||||
set_account_id_if_missing(&mut request.account_id, access_token);
|
||||
changes = self
|
||||
.changes(
|
||||
build_changes_request(&request),
|
||||
MethodObject::FileNode,
|
||||
access_token,
|
||||
)
|
||||
.await?
|
||||
.response;
|
||||
let calculate_total = request.calculate_total.unwrap_or(false);
|
||||
has_changes = changes.has_changes();
|
||||
response = build_query_changes_response(&request, &changes);
|
||||
|
||||
if !has_changes && !calculate_total {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
up_to_id = request.up_to_id;
|
||||
results = self.file_node_query(request.into(), access_token).await?;
|
||||
}
|
||||
QueryChangesRequestMethod::Principal(_) => {
|
||||
return Err(trc::JmapEvent::CannotCalculateChanges.into_err());
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ impl ContactCardQuery for Server {
|
||||
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request
|
||||
.sort
|
||||
.and_then(|s| if !s.is_empty() { s.into() } else { None })
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| vec![Comparator::descending(ContactCardComparator::Updated)])
|
||||
{
|
||||
comparators.push(match comparator.property {
|
||||
|
||||
@@ -333,7 +333,7 @@ impl EmailQuery for Server {
|
||||
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request
|
||||
.sort
|
||||
.and_then(|s| if !s.is_empty() { s.into() } else { None })
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| vec![Comparator::descending(EmailComparator::ReceivedAt)])
|
||||
{
|
||||
comparators.push(match comparator.property {
|
||||
|
||||
221
crates/jmap/src/file/get.rs
Normal file
221
crates/jmap/src/file/get.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* 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, FileNodeProperty, FileNodeValue},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use jmap_tools::{Map, Value};
|
||||
use store::{roaring::RoaringBitmap, write::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 = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
FileNodeProperty::Id,
|
||||
FileNodeProperty::Name,
|
||||
FileNodeProperty::ParentId,
|
||||
FileNodeProperty::Size,
|
||||
]);
|
||||
let account_id = request.account_id.document_id();
|
||||
let cache = self
|
||||
.fetch_dav_resources(access_token, account_id, SyncCollection::FileNode)
|
||||
.await?;
|
||||
let file_node_ids = if access_token.is_member(account_id) {
|
||||
cache.document_ids(true).collect::<RoaringBitmap>()
|
||||
} else {
|
||||
cache.shared_containers(access_token, [Acl::Read, Acl::ReadItems], true)
|
||||
};
|
||||
|
||||
let ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
file_node_ids
|
||||
.iter()
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
state: cache.get_state(true).into(),
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: vec![],
|
||||
};
|
||||
|
||||
for id in ids {
|
||||
// Obtain the file_node object
|
||||
let document_id = id.document_id();
|
||||
if !file_node_ids.contains(document_id) {
|
||||
response.not_found.push(id);
|
||||
continue;
|
||||
}
|
||||
let _file_node = if let Some(file_node) = self
|
||||
.get_archive(account_id, Collection::FileNode, document_id)
|
||||
.await?
|
||||
{
|
||||
file_node
|
||||
} else {
|
||||
response.not_found.push(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().and_then(|f| f.media_type.as_ref())
|
||||
{
|
||||
Value::Str(file.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 => {
|
||||
result.insert_unchecked(
|
||||
FileNodeProperty::Accessed,
|
||||
Value::Element(FileNodeValue::Date(UTCDate::from_timestamp(
|
||||
now() as i64
|
||||
))),
|
||||
);
|
||||
}
|
||||
property => {
|
||||
result.insert_unchecked(property.clone(), Value::Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
response.list.push(result.into());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
9
crates/jmap/src/file/mod.rs
Normal file
9
crates/jmap/src/file/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod get;
|
||||
pub mod query;
|
||||
pub mod set;
|
||||
180
crates/jmap/src/file/query.rs
Normal file
180
crates/jmap/src/file/query.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{JmapMethods, changes::state::JmapCacheState};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use jmap_proto::{
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::file_node::{FileNode, FileNodeFilter},
|
||||
request::MaybeInvalid,
|
||||
};
|
||||
use store::{query, roaring::RoaringBitmap};
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{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, SyncCollection::FileNode)
|
||||
.await?;
|
||||
let filter_mask = (access_token.is_shared(account_id))
|
||||
.then(|| cache.shared_items(access_token, [Acl::ReadItems], true));
|
||||
|
||||
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(query::Filter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.subtree(resource.path()).map(|r| r.document_id()),
|
||||
)))
|
||||
} else {
|
||||
filters.push(query::Filter::is_in_set(RoaringBitmap::new()));
|
||||
}
|
||||
}
|
||||
FileNodeFilter::ParentId(MaybeInvalid::Value(id)) => {
|
||||
filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.children_ids(id.document_id()),
|
||||
)));
|
||||
}
|
||||
FileNodeFilter::HasParentId(has_parent_id) => {
|
||||
filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.resources.iter().filter_map(|r| {
|
||||
if has_parent_id == r.parent_id().is_some() {
|
||||
Some(r.document_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
)));
|
||||
}
|
||||
FileNodeFilter::Name(name) => {
|
||||
filters.push(query::Filter::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(query::Filter::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(query::Filter::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(query::Filter::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
|
||||
}
|
||||
}),
|
||||
)));
|
||||
}
|
||||
unsupported => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter
|
||||
.into_err()
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
},
|
||||
|
||||
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
|
||||
filters.push(cond.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result_set = self
|
||||
.filter(account_id, Collection::FileNode, filters)
|
||||
.await?;
|
||||
|
||||
if let Some(filter_mask) = filter_mask {
|
||||
result_set.apply_mask(filter_mask);
|
||||
}
|
||||
|
||||
let (response, paginate) = self
|
||||
.build_query_response(&result_set, cache.get_state(false), &request)
|
||||
.await?;
|
||||
|
||||
if let Some(paginate) = paginate {
|
||||
// Parse sort criteria
|
||||
/*let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request
|
||||
.sort
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| vec![Comparator::descending(FileNodeComparator::Updated)])
|
||||
{
|
||||
comparators.push(match comparator.property {
|
||||
FileNodeComparator::Created => {
|
||||
query::Comparator::field(ContactField::Created, comparator.is_ascending)
|
||||
}
|
||||
FileNodeComparator::Updated => {
|
||||
query::Comparator::field(ContactField::Updated, comparator.is_ascending)
|
||||
}
|
||||
unsupported => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
});
|
||||
}*/
|
||||
|
||||
if request.sort.is_some_and(|s| !s.is_empty()) {
|
||||
return Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details("Sorting is not supported on FileNode"));
|
||||
}
|
||||
|
||||
// Sort results
|
||||
self.sort(result_set, Default::default(), paginate, response)
|
||||
.await
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
425
crates/jmap/src/file/set.rs
Normal file
425
crates/jmap/src/file/set.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::api::acl::{JmapAcl, JmapRights};
|
||||
use common::{DavResources, Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use groupware::{DestroyArchive, cache::GroupwareCache, file::FileNode};
|
||||
use http_proto::HttpSessionData;
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
method::set::{SetRequest, SetResponse},
|
||||
object::file_node::{self, FileNodeProperty, FileNodeValue},
|
||||
references::resolve::ResolveCreatedReference,
|
||||
request::IntoValid,
|
||||
types::state::State,
|
||||
};
|
||||
use jmap_tools::{JsonPointerItem, Key, Value};
|
||||
use store::{ahash::AHashSet, write::BatchBuilder};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::{Acl, AclGrant},
|
||||
collection::{Collection, SyncCollection},
|
||||
id::Id,
|
||||
};
|
||||
|
||||
pub trait FileNodeSet: Sync + Send {
|
||||
fn file_node_set(
|
||||
&self,
|
||||
request: SetRequest<'_, file_node::FileNode>,
|
||||
access_token: &AccessToken,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = trc::Result<SetResponse<file_node::FileNode>>> + Send;
|
||||
}
|
||||
|
||||
impl FileNodeSet for Server {
|
||||
async fn file_node_set(
|
||||
&self,
|
||||
mut request: SetRequest<'_, file_node::FileNode>,
|
||||
access_token: &AccessToken,
|
||||
_session: &HttpSessionData,
|
||||
) -> trc::Result<SetResponse<file_node::FileNode>> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let cache = self
|
||||
.fetch_dav_resources(access_token, account_id, SyncCollection::FileNode)
|
||||
.await?;
|
||||
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?;
|
||||
let will_destroy = request.unwrap_destroy().into_valid().collect::<Vec<_>>();
|
||||
let is_shared = access_token.is_shared(account_id);
|
||||
|
||||
// Process creates
|
||||
let mut batch = BatchBuilder::new();
|
||||
'create: for (id, object) in request.unwrap_create() {
|
||||
if is_shared {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::forbidden()
|
||||
.with_description("Cannot create file nodes in a shared account."),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
let mut file_node = FileNode::default();
|
||||
|
||||
// Process changes
|
||||
if let Err(err) = update_file_node(object, &mut file_node, &mut response) {
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
// Validate hierarchy
|
||||
if let Err(err) =
|
||||
validate_file_node_hierarchy(None, file_node.parent_id, is_shared, &cache)
|
||||
{
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
// Validate ACLs
|
||||
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;
|
||||
}
|
||||
|
||||
// Insert record
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::FileNode, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
file_node
|
||||
.insert(access_token, account_id, document_id, &mut batch)
|
||||
.caused_by(trc::location!())?;
|
||||
response.created(id, document_id);
|
||||
}
|
||||
|
||||
// Process updates
|
||||
'update: for (id, object) in request.unwrap_update().into_valid() {
|
||||
// Make sure id won't be destroyed
|
||||
if will_destroy.contains(&id) {
|
||||
response.not_updated.append(id, SetError::will_destroy());
|
||||
continue 'update;
|
||||
}
|
||||
|
||||
// Obtain file node
|
||||
let document_id = id.document_id();
|
||||
let file_node_ = if let Some(file_node_) = self
|
||||
.get_archive(account_id, Collection::FileNode, document_id)
|
||||
.await?
|
||||
{
|
||||
file_node_
|
||||
} else {
|
||||
response.not_updated.append(id, SetError::not_found());
|
||||
continue 'update;
|
||||
};
|
||||
let file_node = file_node_
|
||||
.to_unarchived::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_file_node = file_node
|
||||
.deserialize::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Apply changes
|
||||
let has_acl_changes = match update_file_node(object, &mut new_file_node, &mut response)
|
||||
{
|
||||
Ok(has_acl_changes_) => has_acl_changes_,
|
||||
Err(err) => {
|
||||
response.not_updated.append(id, err);
|
||||
continue 'update;
|
||||
}
|
||||
};
|
||||
|
||||
// Validate hierarchy
|
||||
if new_file_node.parent_id != file_node.inner.parent_id
|
||||
&& let Err(err) = validate_file_node_hierarchy(
|
||||
Some(document_id),
|
||||
new_file_node.parent_id,
|
||||
is_shared,
|
||||
&cache,
|
||||
)
|
||||
{
|
||||
response.not_updated.append(id, err);
|
||||
continue 'update;
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if is_shared {
|
||||
let acl = file_node.inner.acls.effective_acl(access_token);
|
||||
if !acl.contains(Acl::Modify) || (has_acl_changes && !acl.contains(Acl::Administer))
|
||||
{
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::forbidden()
|
||||
.with_description("You are not allowed to modify this file node."),
|
||||
);
|
||||
continue 'update;
|
||||
}
|
||||
}
|
||||
if has_acl_changes {
|
||||
if let Err(err) = self.acl_validate(&new_file_node.acls).await {
|
||||
response.not_updated.append(id, err.into());
|
||||
continue 'update;
|
||||
}
|
||||
self.refresh_acls(
|
||||
&new_file_node.acls,
|
||||
Some(
|
||||
file_node
|
||||
.inner
|
||||
.acls
|
||||
.iter()
|
||||
.map(AclGrant::from)
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Update record
|
||||
new_file_node
|
||||
.update(access_token, file_node, account_id, document_id, &mut batch)
|
||||
.caused_by(trc::location!())?;
|
||||
response.updated.append(id, None);
|
||||
}
|
||||
|
||||
// Process deletions
|
||||
let on_destroy_remove_children = request
|
||||
.arguments
|
||||
.on_destroy_remove_children
|
||||
.unwrap_or(false);
|
||||
let mut destroy_ids = AHashSet::with_capacity(will_destroy.len());
|
||||
for id in will_destroy {
|
||||
let document_id = id.document_id();
|
||||
|
||||
let Some(file_node) = cache.container_resource_path_by_id(document_id) else {
|
||||
response.not_destroyed.append(id, SetError::not_found());
|
||||
continue;
|
||||
};
|
||||
|
||||
// Find ids to delete
|
||||
let mut ids = cache.subtree(file_node.path()).collect::<Vec<_>>();
|
||||
if ids.is_empty() {
|
||||
debug_assert!(false, "Resource found in cache but not in subtree");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sort ids descending from the deepest to the root
|
||||
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()));
|
||||
|
||||
// Validate not already deleted
|
||||
for child_id in &sorted_ids {
|
||||
if !destroy_ids.insert(*child_id) {
|
||||
response.not_destroyed.append(
|
||||
id,
|
||||
SetError::will_destroy().with_description(
|
||||
"File node or one of its children is already marked for deletion.",
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate ACLs
|
||||
if !access_token.is_member(account_id) {
|
||||
let permissions = cache.shared_containers(access_token, [Acl::Delete], false);
|
||||
if permissions.len() < sorted_ids.len() as u64
|
||||
|| !sorted_ids.iter().all(|id| permissions.contains(*id))
|
||||
{
|
||||
response.not_destroyed.append(
|
||||
id,
|
||||
SetError::forbidden()
|
||||
.with_description("You are not allowed to delete this file node."),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain children ids
|
||||
if sorted_ids.len() > 1 && !on_destroy_remove_children {
|
||||
response
|
||||
.not_destroyed
|
||||
.append(id, SetError::node_has_children());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delete record
|
||||
response
|
||||
.destroyed
|
||||
.extend(sorted_ids.iter().copied().map(Id::from));
|
||||
|
||||
DestroyArchive(sorted_ids)
|
||||
.delete_batch(
|
||||
self,
|
||||
access_token,
|
||||
account_id,
|
||||
cache.format_resource(file_node).into(),
|
||||
&mut batch,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Write changes
|
||||
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).into();
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn update_file_node(
|
||||
updates: Value<'_, FileNodeProperty, FileNodeValue>,
|
||||
file_node: &mut FileNode,
|
||||
response: &mut SetResponse<file_node::FileNode>,
|
||||
) -> Result<bool, SetError<FileNodeProperty>> {
|
||||
let mut has_acl_changes = false;
|
||||
|
||||
for (property, mut value) in updates.into_expanded_object() {
|
||||
let Key::Property(property) = property else {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(property.to_owned())
|
||||
.with_description("Invalid property."));
|
||||
};
|
||||
|
||||
response.resolve_self_references(&mut value)?;
|
||||
|
||||
match (property, value) {
|
||||
(FileNodeProperty::Name, Value::Str(value))
|
||||
if (1..=255).contains(&value.len())
|
||||
&& !value.contains('/')
|
||||
&& ![".", ".."].contains(&value.as_ref()) =>
|
||||
{
|
||||
file_node.name = value.into_owned();
|
||||
}
|
||||
(FileNodeProperty::ParentId, Value::Element(FileNodeValue::Id(value))) => {
|
||||
file_node.parent_id = value.document_id() + 1;
|
||||
}
|
||||
(FileNodeProperty::ParentId, Value::Null) => {
|
||||
file_node.parent_id = 0;
|
||||
}
|
||||
(FileNodeProperty::BlobId, Value::Element(FileNodeValue::BlobId(value))) => {
|
||||
file_node.file.get_or_insert_default().blob_hash = value.hash;
|
||||
}
|
||||
(FileNodeProperty::BlobId, Value::Null) => {}
|
||||
(FileNodeProperty::Size, Value::Number(value)) => {
|
||||
file_node.file.get_or_insert_default().size = value.cast_to_u64() as u32;
|
||||
}
|
||||
(FileNodeProperty::Type, Value::Str(value)) if (1..=30).contains(&value.len()) => {
|
||||
file_node.file.get_or_insert_default().media_type = value.into_owned().into();
|
||||
}
|
||||
(FileNodeProperty::Type, Value::Null) => {
|
||||
file_node.file.get_or_insert_default().media_type = None;
|
||||
}
|
||||
(FileNodeProperty::Executable, Value::Bool(value)) => {
|
||||
file_node.file.get_or_insert_default().executable = value;
|
||||
}
|
||||
(FileNodeProperty::Executable, Value::Null) => {
|
||||
file_node.file.get_or_insert_default().executable = false;
|
||||
}
|
||||
(FileNodeProperty::Created, Value::Element(FileNodeValue::Date(value))) => {
|
||||
file_node.created = value.timestamp();
|
||||
}
|
||||
(FileNodeProperty::Modified, Value::Element(FileNodeValue::Date(value))) => {
|
||||
file_node.modified = value.timestamp();
|
||||
}
|
||||
(FileNodeProperty::ShareWith, value) => {
|
||||
file_node.acls = JmapRights::acl_set::<file_node::FileNode>(value)?;
|
||||
has_acl_changes = true;
|
||||
}
|
||||
(FileNodeProperty::Pointer(pointer), value)
|
||||
if matches!(
|
||||
pointer.first(),
|
||||
Some(JsonPointerItem::Key(Key::Property(
|
||||
FileNodeProperty::ShareWith
|
||||
)))
|
||||
) =>
|
||||
{
|
||||
let mut pointer = pointer.iter();
|
||||
pointer.next();
|
||||
|
||||
file_node.acls = JmapRights::acl_patch::<file_node::FileNode>(
|
||||
std::mem::take(&mut file_node.acls),
|
||||
pointer,
|
||||
value,
|
||||
)?;
|
||||
has_acl_changes = true;
|
||||
}
|
||||
(property, _) => {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(property.clone())
|
||||
.with_description("Field could not be set."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate name
|
||||
if file_node.name.is_empty() {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(FileNodeProperty::Name)
|
||||
.with_description("Missing name."));
|
||||
}
|
||||
|
||||
// Validate blob hash
|
||||
if file_node
|
||||
.file
|
||||
.as_ref()
|
||||
.is_some_and(|f| f.blob_hash.is_empty())
|
||||
{
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(FileNodeProperty::BlobId)
|
||||
.with_description("Missing blob id."));
|
||||
}
|
||||
|
||||
Ok(has_acl_changes)
|
||||
}
|
||||
|
||||
fn validate_file_node_hierarchy(
|
||||
document_id: Option<u32>,
|
||||
parent_id: u32,
|
||||
is_shared: bool,
|
||||
cache: &DavResources,
|
||||
) -> Result<(), SetError<FileNodeProperty>> {
|
||||
if parent_id == 0 {
|
||||
if is_shared {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(FileNodeProperty::ParentId)
|
||||
.with_description("Cannot create top-level folder in a shared account."));
|
||||
}
|
||||
} else {
|
||||
let parent_id = parent_id - 1;
|
||||
|
||||
if let Some(document_id) = document_id {
|
||||
if document_id == parent_id {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(FileNodeProperty::ParentId)
|
||||
.with_description("A file node cannot be its own parent."));
|
||||
}
|
||||
|
||||
if let Some(file) = cache.container_resource_path_by_id(document_id)
|
||||
&& cache
|
||||
.subtree(file.path())
|
||||
.any(|r| r.document_id() == parent_id)
|
||||
{
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(FileNodeProperty::ParentId)
|
||||
.with_description("Circular reference in parent ids."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -30,6 +30,7 @@ pub mod blob;
|
||||
pub mod changes;
|
||||
pub mod contact;
|
||||
pub mod email;
|
||||
pub mod file;
|
||||
pub mod identity;
|
||||
pub mod mailbox;
|
||||
pub mod principal;
|
||||
|
||||
@@ -71,7 +71,7 @@ impl SieveScriptQuery for Server {
|
||||
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request
|
||||
.sort
|
||||
.and_then(|s| if !s.is_empty() { s.into() } else { None })
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| vec![Comparator::descending(SieveComparator::Name)])
|
||||
{
|
||||
comparators.push(match comparator.property {
|
||||
|
||||
@@ -120,7 +120,7 @@ impl EmailSubmissionQuery for Server {
|
||||
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request
|
||||
.sort
|
||||
.and_then(|s| if !s.is_empty() { s.into() } else { None })
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| vec![Comparator::descending(EmailSubmissionComparator::SentAt)])
|
||||
{
|
||||
comparators.push(match comparator.property {
|
||||
|
||||
@@ -49,6 +49,10 @@ impl BlobHash {
|
||||
}
|
||||
hex
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0 == [0; BLOB_HASH_LEN]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ArchivedBlobHash> for BlobHash {
|
||||
|
||||
@@ -47,11 +47,6 @@ pub enum EmailField {
|
||||
To,
|
||||
Cc,
|
||||
Bcc,
|
||||
//ReplyTo,
|
||||
//Sender,
|
||||
//InReplyTo,
|
||||
//MessageId,
|
||||
//EmailIds,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -127,11 +122,6 @@ impl From<EmailField> for u8 {
|
||||
EmailField::SentAt => 26,
|
||||
EmailField::HasAttachment => 89,
|
||||
EmailField::Archive => ARCHIVE_FIELD,
|
||||
//EmailField::MessageId => 11,
|
||||
//EmailField::ReplyTo => 21,
|
||||
//EmailField::Sender => 25,
|
||||
//EmailField::EmailIds => 84,
|
||||
//EmailField::InReplyTo => 96,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GlobPattern {
|
||||
@@ -207,3 +210,15 @@ impl<V> Default for GlobMap<V> {
|
||||
GlobMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for GlobPattern {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Ok(GlobPattern::compile(
|
||||
<Cow<&str>>::deserialize(deserializer)?.as_ref(),
|
||||
true,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user