DAV storage methods
This commit is contained in:
56
crates/dav/src/common/acl.rs
Normal file
56
crates/dav/src/common/acl.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use common::{Server, auth::AccessToken};
|
||||
use hyper::StatusCode;
|
||||
use jmap_proto::types::{acl::Acl, collection::Collection};
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::DavError;
|
||||
|
||||
pub(crate) trait DavAclHandler: Sync + Send {
|
||||
fn validate_and_map_parent_acl(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
parent_id: Option<u32>,
|
||||
check_acls: Acl,
|
||||
) -> impl Future<Output = crate::Result<u32>> + Send;
|
||||
}
|
||||
|
||||
impl DavAclHandler for Server {
|
||||
async fn validate_and_map_parent_acl(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
parent_id: Option<u32>,
|
||||
check_acls: Acl,
|
||||
) -> crate::Result<u32> {
|
||||
match parent_id {
|
||||
Some(parent_id) => {
|
||||
if access_token.is_member(account_id)
|
||||
|| self
|
||||
.has_access_to_document(
|
||||
access_token,
|
||||
account_id,
|
||||
collection,
|
||||
parent_id,
|
||||
check_acls,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
Ok(parent_id + 1)
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::FORBIDDEN))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if access_token.is_member(account_id) {
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::FORBIDDEN))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
crates/dav/src/common/mod.rs
Normal file
2
crates/dav/src/common/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod acl;
|
||||
pub mod uri;
|
||||
84
crates/dav/src/common/uri.rs
Normal file
84
crates/dav/src/common/uri.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use common::{Server, auth::AccessToken};
|
||||
|
||||
use directory::backend::internal::manage::ManageDirectory;
|
||||
use http_proto::request::decode_path_element;
|
||||
use hyper::StatusCode;
|
||||
use jmap_proto::types::collection::Collection;
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::{DavError, DavResource};
|
||||
|
||||
pub(crate) struct UriResource<T> {
|
||||
pub collection: Collection,
|
||||
pub account_id: Option<u32>,
|
||||
pub resource: T,
|
||||
}
|
||||
|
||||
pub(crate) trait DavUriResource: Sync + Send {
|
||||
fn validate_uri<'x>(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
uri: &'x str,
|
||||
) -> impl Future<Output = crate::Result<UriResource<Option<&'x str>>>> + Send;
|
||||
}
|
||||
|
||||
impl DavUriResource for Server {
|
||||
async fn validate_uri<'x>(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
uri: &'x str,
|
||||
) -> crate::Result<UriResource<Option<&'x str>>> {
|
||||
let (_, uri_parts) = uri
|
||||
.split_once("/dav/")
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
let mut uri_parts = uri_parts.splitn(3, '/').filter(|x| !x.is_empty());
|
||||
let mut resource = UriResource {
|
||||
collection: uri_parts
|
||||
.next()
|
||||
.and_then(DavResource::parse)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
|
||||
.into(),
|
||||
account_id: None,
|
||||
resource: None,
|
||||
};
|
||||
if let Some(account) = uri_parts.next() {
|
||||
// Parse account id
|
||||
let account_id = if let Some(account_id) = account.strip_prefix('_') {
|
||||
account_id
|
||||
.parse::<u32>()
|
||||
.map_err(|_| DavError::Code(StatusCode::NOT_FOUND))?
|
||||
} else {
|
||||
let account = decode_path_element(account);
|
||||
if access_token.name == account {
|
||||
access_token.primary_id
|
||||
} else {
|
||||
self.store()
|
||||
.get_principal_id(&account)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
|
||||
}
|
||||
};
|
||||
|
||||
// Validate access
|
||||
if resource.collection != Collection::Principal
|
||||
&& !access_token.has_access(account_id, resource.collection)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Obtain remaining path
|
||||
resource.account_id = Some(account_id);
|
||||
resource.resource = uri_parts.next().map(|uri| uri.trim_end_matches('/'));
|
||||
}
|
||||
|
||||
Ok(resource)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UriResource<T> {
|
||||
pub fn account_id(&self) -> crate::Result<u32> {
|
||||
self.account_id.ok_or(DavError::Code(StatusCode::NOT_FOUND))
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,32 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{RequestHeaders, schema::request::Acl};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use dav_proto::RequestHeaders;
|
||||
use groupware::file::ArchivedFileNode;
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use jmap_proto::types::{acl::Acl, collection::Collection};
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::DavError;
|
||||
|
||||
pub(crate) trait FileAclRequestHandler: Sync + Send {
|
||||
fn handle_file_acl_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: RequestHeaders<'_>,
|
||||
request: Acl,
|
||||
request: dav_proto::schema::request::Acl,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn validate_file_acl(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
node: &ArchivedFileNode,
|
||||
acl_child: Acl,
|
||||
acl_parent: Acl,
|
||||
) -> impl Future<Output = crate::Result<()>> + Send;
|
||||
}
|
||||
|
||||
impl FileAclRequestHandler for Server {
|
||||
@@ -22,8 +37,36 @@ impl FileAclRequestHandler for Server {
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: RequestHeaders<'_>,
|
||||
request: Acl,
|
||||
request: dav_proto::schema::request::Acl,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn validate_file_acl(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
node: &ArchivedFileNode,
|
||||
acl_child: Acl,
|
||||
acl_parent: Acl,
|
||||
) -> crate::Result<()> {
|
||||
if access_token.is_member(account_id)
|
||||
|| node.acls.effective_acl(access_token).contains(acl_child)
|
||||
|| (u32::from(node.parent_id) > 0
|
||||
&& self
|
||||
.has_access_to_document(
|
||||
access_token,
|
||||
account_id,
|
||||
Collection::FileNode,
|
||||
u32::from(node.parent_id) - 1,
|
||||
acl_parent,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::FORBIDDEN))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,20 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::RequestHeaders;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime};
|
||||
use groupware::file::{FileNode, hierarchy::FileHierarchy};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use jmap_proto::types::{acl::Acl, collection::Collection, property::Property};
|
||||
use store::write::Archive;
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::{
|
||||
DavError,
|
||||
common::uri::DavUriResource,
|
||||
file::{DavFileResource, acl::FileAclRequestHandler},
|
||||
};
|
||||
|
||||
pub(crate) trait FileGetRequestHandler: Sync + Send {
|
||||
fn handle_file_get_request(
|
||||
@@ -24,6 +35,57 @@ impl FileGetRequestHandler for Server {
|
||||
headers: RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
todo!()
|
||||
// Validate URI
|
||||
let resource = self.validate_uri(access_token, headers.uri).await?;
|
||||
let account_id = resource.account_id()?;
|
||||
let files = self
|
||||
.fetch_file_hierarchy(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = files.map_resource(resource)?;
|
||||
|
||||
// Fetch node
|
||||
let node_ = self
|
||||
.get_property::<Archive>(
|
||||
account_id,
|
||||
Collection::FileNode,
|
||||
resource.resource,
|
||||
Property::Value,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let node = node_.unarchive::<FileNode>().caused_by(trc::location!())?;
|
||||
|
||||
// Validate ACL
|
||||
self.validate_file_acl(access_token, account_id, node, Acl::Read, Acl::ReadItems)
|
||||
.await?;
|
||||
|
||||
let (hash, size, content_type) = if let Some(file) = node.file.as_ref() {
|
||||
(
|
||||
file.blob_hash.0.as_ref(),
|
||||
u32::from(file.size) as usize,
|
||||
file.media_type.as_ref().map(|s| s.as_str()),
|
||||
)
|
||||
} else {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
};
|
||||
|
||||
let response = HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type(content_type.unwrap_or("application/octet-stream"))
|
||||
.with_etag(u64::from(node.change_id))
|
||||
.with_last_modified(Rfc1123DateTime::new(i64::from(node.modified)).to_string());
|
||||
|
||||
if !is_head {
|
||||
Ok(response.with_binary_body(
|
||||
self.blob_store()
|
||||
.get_blob(hash, 0..usize::MAX)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?,
|
||||
))
|
||||
} else {
|
||||
Ok(response.with_content_length(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,24 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{RequestHeaders, schema::request::MkCol};
|
||||
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
schema::{Namespace, request::MkCol, response::MkColResponse},
|
||||
};
|
||||
use groupware::file::{FileNode, hierarchy::FileHierarchy};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use jmap_proto::types::{acl::Acl, collection::Collection};
|
||||
use store::write::{BatchBuilder, log::LogInsert, now};
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::{
|
||||
common::{acl::DavAclHandler, uri::DavUriResource},
|
||||
file::DavFileResource,
|
||||
};
|
||||
|
||||
use super::proppatch::FilePropPatchRequestHandler;
|
||||
|
||||
pub(crate) trait FileMkColRequestHandler: Sync + Send {
|
||||
fn handle_file_mkcol_request(
|
||||
@@ -24,6 +39,66 @@ impl FileMkColRequestHandler for Server {
|
||||
headers: RequestHeaders<'_>,
|
||||
request: Option<MkCol>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
todo!()
|
||||
// Validate URI
|
||||
let resource = self.validate_uri(access_token, headers.uri).await?;
|
||||
let account_id = resource.account_id()?;
|
||||
let files = self
|
||||
.fetch_file_hierarchy(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = files.map_parent_resource(resource)?;
|
||||
|
||||
// Build collection
|
||||
let parent_id = self
|
||||
.validate_and_map_parent_acl(
|
||||
access_token,
|
||||
account_id,
|
||||
Collection::FileNode,
|
||||
resource.resource.0,
|
||||
Acl::CreateChild,
|
||||
)
|
||||
.await?;
|
||||
let change_id = self.generate_snowflake_id().caused_by(trc::location!())?;
|
||||
let now = now();
|
||||
let mut node = FileNode {
|
||||
parent_id,
|
||||
name: resource.resource.1.into_owned(),
|
||||
display_name: None,
|
||||
file: None,
|
||||
created: now as i64,
|
||||
modified: now as i64,
|
||||
change_id,
|
||||
dead_properties: Default::default(),
|
||||
acls: Default::default(),
|
||||
};
|
||||
|
||||
// Apply MKCOL properties
|
||||
if let Some(mkcol) = request {
|
||||
let mut prop_stat = Vec::new();
|
||||
if !self.apply_file_properties(&mut node, false, mkcol.props, &mut prop_stat) {
|
||||
return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body(
|
||||
MkColResponse::new(prop_stat)
|
||||
.with_namespace(Namespace::Dav)
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_change_id(change_id)
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.create_document()
|
||||
.log(LogInsert())
|
||||
.custom(ObjectIndexBuilder::new().with_changes(node))
|
||||
.caused_by(trc::location!())?;
|
||||
self.store()
|
||||
.write(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod acl;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use common::Files;
|
||||
use hyper::StatusCode;
|
||||
|
||||
use crate::{DavError, common::uri::UriResource};
|
||||
|
||||
pub mod acl;
|
||||
pub mod changes;
|
||||
pub mod copy_move;
|
||||
pub mod delete;
|
||||
@@ -15,8 +22,92 @@ pub mod propfind;
|
||||
pub mod proppatch;
|
||||
pub mod update;
|
||||
|
||||
pub(crate) enum UpdateType {
|
||||
Post(Vec<u8>),
|
||||
Put(Vec<u8>),
|
||||
Patch(Vec<u8>),
|
||||
pub(crate) trait DavFileResource {
|
||||
fn map_resource(&self, resource: UriResource<Option<&str>>) -> crate::Result<UriResource<u32>>;
|
||||
|
||||
fn map_resource_or_root(
|
||||
&self,
|
||||
resource: UriResource<Option<&str>>,
|
||||
) -> crate::Result<UriResource<Option<u32>>>;
|
||||
|
||||
fn map_parent<'x>(&self, resource: &'x str) -> crate::Result<(Option<u32>, Cow<'x, str>)>;
|
||||
|
||||
fn map_parent_resource<'x>(
|
||||
&self,
|
||||
resource: UriResource<Option<&'x str>>,
|
||||
) -> crate::Result<UriResource<(Option<u32>, Cow<'x, str>)>>;
|
||||
}
|
||||
|
||||
impl DavFileResource for Files {
|
||||
fn map_resource(&self, resource: UriResource<Option<&str>>) -> crate::Result<UriResource<u32>> {
|
||||
resource
|
||||
.resource
|
||||
.and_then(|r| self.files.by_name(r))
|
||||
.map(|r| UriResource {
|
||||
collection: resource.collection,
|
||||
account_id: resource.account_id,
|
||||
resource: r,
|
||||
})
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))
|
||||
}
|
||||
|
||||
fn map_resource_or_root(
|
||||
&self,
|
||||
resource: UriResource<Option<&str>>,
|
||||
) -> crate::Result<UriResource<Option<u32>>> {
|
||||
Ok(UriResource {
|
||||
collection: resource.collection,
|
||||
account_id: resource.account_id,
|
||||
resource: if let Some(resource) = resource.resource {
|
||||
Some(
|
||||
self.files
|
||||
.by_name(resource)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn map_parent<'x>(&self, resource: &'x str) -> crate::Result<(Option<u32>, Cow<'x, str>)> {
|
||||
let (parent, child) = if let Some((parent, child)) = resource.rsplit_once('/') {
|
||||
(
|
||||
Some(
|
||||
self.files
|
||||
.by_name(parent)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?,
|
||||
),
|
||||
child,
|
||||
)
|
||||
} else {
|
||||
(None, resource)
|
||||
};
|
||||
|
||||
Ok((
|
||||
parent,
|
||||
percent_encoding::percent_decode_str(child)
|
||||
.decode_utf8()
|
||||
.unwrap_or_else(|_| child.into()),
|
||||
))
|
||||
}
|
||||
|
||||
fn map_parent_resource<'x>(
|
||||
&self,
|
||||
resource: UriResource<Option<&'x str>>,
|
||||
) -> crate::Result<UriResource<(Option<u32>, Cow<'x, str>)>> {
|
||||
if let Some(r) = resource.resource {
|
||||
if self.files.by_name(r).is_none() {
|
||||
self.map_parent(r).map(|r| UriResource {
|
||||
collection: resource.collection,
|
||||
account_id: resource.account_id,
|
||||
resource: r,
|
||||
})
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
|
||||
}
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,27 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{RequestHeaders, schema::request::PropertyUpdate};
|
||||
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
schema::{
|
||||
property::{DavProperty, DavValue, ResourceType, WebDavProperty},
|
||||
request::{DavPropertyValue, PropertyUpdate},
|
||||
response::{BaseCondition, MultiStatus, PropStat, Response},
|
||||
},
|
||||
};
|
||||
use groupware::file::{FileNode, hierarchy::FileHierarchy};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use jmap_proto::types::{acl::Acl, collection::Collection, property::Property};
|
||||
use store::write::{Archive, BatchBuilder, assert::HashedValue, log::Changes, now};
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::{
|
||||
DavError,
|
||||
common::uri::DavUriResource,
|
||||
file::{DavFileResource, acl::FileAclRequestHandler},
|
||||
};
|
||||
|
||||
pub(crate) trait FilePropPatchRequestHandler: Sync + Send {
|
||||
fn handle_file_proppatch_request(
|
||||
@@ -15,6 +33,14 @@ pub(crate) trait FilePropPatchRequestHandler: Sync + Send {
|
||||
headers: RequestHeaders<'_>,
|
||||
request: PropertyUpdate,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn apply_file_properties(
|
||||
&self,
|
||||
file: &mut FileNode,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut Vec<PropStat>,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
impl FilePropPatchRequestHandler for Server {
|
||||
@@ -24,6 +50,211 @@ impl FilePropPatchRequestHandler for Server {
|
||||
headers: RequestHeaders<'_>,
|
||||
request: PropertyUpdate,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
todo!()
|
||||
// Validate URI
|
||||
let resource = self.validate_uri(access_token, headers.uri).await?;
|
||||
let uri = headers.uri;
|
||||
let account_id = resource.account_id()?;
|
||||
let files = self
|
||||
.fetch_file_hierarchy(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = files.map_resource(resource)?;
|
||||
|
||||
// Fetch node
|
||||
let node_ = self
|
||||
.get_property::<HashedValue<Archive>>(
|
||||
account_id,
|
||||
Collection::FileNode,
|
||||
resource.resource,
|
||||
Property::Value,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let node = node_
|
||||
.to_unarchived::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate ACL
|
||||
self.validate_file_acl(
|
||||
access_token,
|
||||
account_id,
|
||||
node.inner,
|
||||
Acl::Modify,
|
||||
Acl::ModifyItems,
|
||||
)
|
||||
.await?;
|
||||
let node = node.into_deserialized().caused_by(trc::location!())?;
|
||||
let mut new_node = node.inner.clone();
|
||||
|
||||
// Remove properties
|
||||
let mut items = Vec::with_capacity(request.remove.len() + request.set.len());
|
||||
for property in request.remove {
|
||||
match property {
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName) => {
|
||||
new_node.display_name = None;
|
||||
items.push(
|
||||
PropStat::new(DavProperty::WebDav(WebDavProperty::DisplayName))
|
||||
.with_status(StatusCode::OK),
|
||||
);
|
||||
}
|
||||
DavProperty::WebDav(WebDavProperty::GetContentType) if new_node.file.is_some() => {
|
||||
new_node.file.as_mut().unwrap().media_type = None;
|
||||
items.push(
|
||||
PropStat::new(DavProperty::WebDav(WebDavProperty::GetContentType))
|
||||
.with_status(StatusCode::OK),
|
||||
);
|
||||
}
|
||||
DavProperty::DeadProperty(dead) => {
|
||||
new_node.dead_properties.remove_element(&dead);
|
||||
items.push(
|
||||
PropStat::new(DavProperty::DeadProperty(dead)).with_status(StatusCode::OK),
|
||||
);
|
||||
}
|
||||
property => {
|
||||
items.push(
|
||||
PropStat::new(property)
|
||||
.with_status(StatusCode::CONFLICT)
|
||||
.with_response_description("Property cannot be modified"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set properties
|
||||
self.apply_file_properties(&mut new_node, true, request.set, &mut items);
|
||||
|
||||
if new_node != node.inner {
|
||||
// Build node
|
||||
new_node.modified = now() as i64;
|
||||
new_node.change_id = self.generate_snowflake_id().caused_by(trc::location!())?;
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_change_id(new_node.change_id)
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.update_document(resource.resource)
|
||||
.log(Changes::update([resource.resource]))
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(node)
|
||||
.with_changes(new_node)
|
||||
.with_tenant_id(access_token),
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
self.store()
|
||||
.write(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
|
||||
.with_xml_body(MultiStatus::new(vec![Response::new_propstat(uri, items)]).to_string()))
|
||||
}
|
||||
|
||||
fn apply_file_properties(
|
||||
&self,
|
||||
file: &mut FileNode,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut Vec<PropStat>,
|
||||
) -> bool {
|
||||
let mut has_errors = false;
|
||||
|
||||
for property in properties {
|
||||
match (property.property, property.value) {
|
||||
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
|
||||
if name.len() <= self.core.dav.live_property_size {
|
||||
file.display_name = Some(name);
|
||||
items.push(
|
||||
PropStat::new(DavProperty::WebDav(WebDavProperty::DisplayName))
|
||||
.with_status(StatusCode::OK),
|
||||
);
|
||||
} else {
|
||||
items.push(
|
||||
PropStat::new(DavProperty::WebDav(WebDavProperty::DisplayName))
|
||||
.with_status(StatusCode::INSUFFICIENT_STORAGE)
|
||||
.with_response_description("Display name too long"),
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
file.created = dt;
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::GetContentType), DavValue::String(name))
|
||||
if file.file.is_some() =>
|
||||
{
|
||||
if name.len() <= self.core.dav.live_property_size {
|
||||
file.file.as_mut().unwrap().media_type = Some(name);
|
||||
items.push(
|
||||
PropStat::new(DavProperty::WebDav(WebDavProperty::GetContentType))
|
||||
.with_status(StatusCode::OK),
|
||||
);
|
||||
} else {
|
||||
items.push(
|
||||
PropStat::new(DavProperty::WebDav(WebDavProperty::GetContentType))
|
||||
.with_status(StatusCode::INSUFFICIENT_STORAGE)
|
||||
.with_response_description("Content-type is too long"),
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavValue::ResourceTypes(types),
|
||||
) if file.file.is_none() => {
|
||||
if types.0.len() != 1 || types.0.first() != Some(&ResourceType::Collection) {
|
||||
items.push(
|
||||
PropStat::new(DavProperty::WebDav(WebDavProperty::ResourceType))
|
||||
.with_status(StatusCode::FORBIDDEN)
|
||||
.with_error(BaseCondition::ValidResourceType),
|
||||
);
|
||||
has_errors = true;
|
||||
} else {
|
||||
items.push(
|
||||
PropStat::new(DavProperty::WebDav(WebDavProperty::ResourceType))
|
||||
.with_status(StatusCode::OK),
|
||||
);
|
||||
}
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.dav.dead_property_size.is_some() =>
|
||||
{
|
||||
if is_update {
|
||||
file.dead_properties.remove_element(&dead);
|
||||
}
|
||||
|
||||
if file.dead_properties.size() + values.size() + dead.size()
|
||||
< self.core.dav.dead_property_size.unwrap()
|
||||
{
|
||||
file.dead_properties.add_element(dead.clone(), values.0);
|
||||
items.push(
|
||||
PropStat::new(DavProperty::DeadProperty(dead))
|
||||
.with_status(StatusCode::OK),
|
||||
);
|
||||
} else {
|
||||
items.push(
|
||||
PropStat::new(DavProperty::DeadProperty(dead))
|
||||
.with_status(StatusCode::INSUFFICIENT_STORAGE)
|
||||
.with_response_description("Dead property is too large."),
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(property, _) => {
|
||||
items.push(
|
||||
PropStat::new(property)
|
||||
.with_status(StatusCode::CONFLICT)
|
||||
.with_response_description("Property cannot be modified"),
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!has_errors
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,36 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{RequestHeaders, schema::request::SyncCollection};
|
||||
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
|
||||
use dav_proto::RequestHeaders;
|
||||
use groupware::file::{FileNode, FileProperties, hierarchy::FileHierarchy};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use jmap_proto::types::{acl::Acl, collection::Collection, property::Property};
|
||||
use store::write::{
|
||||
Archive, BatchBuilder,
|
||||
assert::HashedValue,
|
||||
log::{Changes, LogInsert},
|
||||
now,
|
||||
};
|
||||
use trc::AddContext;
|
||||
use utils::BlobHash;
|
||||
|
||||
use super::UpdateType;
|
||||
use crate::{
|
||||
DavError,
|
||||
common::{acl::DavAclHandler, uri::DavUriResource},
|
||||
file::DavFileResource,
|
||||
};
|
||||
|
||||
use super::acl::FileAclRequestHandler;
|
||||
|
||||
pub(crate) trait FileUpdateRequestHandler: Sync + Send {
|
||||
fn handle_file_update_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: RequestHeaders<'_>,
|
||||
request: UpdateType,
|
||||
bytes: Vec<u8>,
|
||||
is_patch: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
@@ -24,8 +42,193 @@ impl FileUpdateRequestHandler for Server {
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: RequestHeaders<'_>,
|
||||
request: UpdateType,
|
||||
bytes: Vec<u8>,
|
||||
_is_patch: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
todo!()
|
||||
// Validate URI
|
||||
let resource = self.validate_uri(access_token, headers.uri).await?;
|
||||
let account_id = resource.account_id()?;
|
||||
let files = self
|
||||
.fetch_file_hierarchy(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource_name = resource
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
if let Some(document_id) = files.files.by_name(resource_name) {
|
||||
// Update
|
||||
let node_archive_ = self
|
||||
.get_property::<HashedValue<Archive>>(
|
||||
account_id,
|
||||
Collection::FileNode,
|
||||
document_id,
|
||||
Property::Value,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let node_archive = node_archive_
|
||||
.to_unarchived::<FileNode>()
|
||||
.caused_by(trc::location!())?;
|
||||
let node = node_archive.inner;
|
||||
|
||||
// Validate ACL
|
||||
self.validate_file_acl(
|
||||
access_token,
|
||||
account_id,
|
||||
node,
|
||||
Acl::Modify,
|
||||
Acl::ModifyItems,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Verify that the node is a file
|
||||
if let Some(file) = node.file.as_ref() {
|
||||
if BlobHash::generate(&bytes).as_slice() == file.blob_hash.0.as_slice() {
|
||||
return Ok(HttpResponse::new(StatusCode::OK));
|
||||
}
|
||||
} else {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
let extra_bytes = (bytes.len() as u64)
|
||||
.saturating_sub(u32::from(node.file.as_ref().unwrap().size) as u64);
|
||||
if extra_bytes > 0 {
|
||||
self.has_available_quota(&access_token.as_resource_token(), extra_bytes)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Write blob
|
||||
let blob_hash = self
|
||||
.put_blob(account_id, &bytes, false)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.hash;
|
||||
|
||||
// Build node
|
||||
let change_id = self.generate_snowflake_id().caused_by(trc::location!())?;
|
||||
let node = node_archive
|
||||
.into_deserialized()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_node = node.inner.clone();
|
||||
let new_file = new_node.file.as_mut().unwrap();
|
||||
new_file.blob_hash = blob_hash;
|
||||
new_file.media_type = headers.content_type.map(|v| v.to_string());
|
||||
new_file.size = bytes.len() as u32;
|
||||
new_node.modified = now() as i64;
|
||||
new_node.change_id = change_id;
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_change_id(change_id)
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.update_document(document_id)
|
||||
.log(Changes::update([document_id]))
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(node)
|
||||
.with_changes(new_node)
|
||||
.with_tenant_id(access_token),
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
self.store()
|
||||
.write(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
} else {
|
||||
// Insert
|
||||
let (parent_id, resource_name) = files.map_parent(resource_name)?;
|
||||
|
||||
// Validate ACL
|
||||
let parent_id = self
|
||||
.validate_and_map_parent_acl(
|
||||
access_token,
|
||||
account_id,
|
||||
Collection::FileNode,
|
||||
parent_id,
|
||||
Acl::AddItems,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Verify that parent is a collection
|
||||
if parent_id > 0
|
||||
&& self
|
||||
.get_property::<Archive>(
|
||||
account_id,
|
||||
Collection::FileNode,
|
||||
parent_id - 1,
|
||||
Property::Value,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
|
||||
.unarchive::<FileNode>()
|
||||
.caused_by(trc::location!())?
|
||||
.file
|
||||
.is_some()
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
if !bytes.is_empty() {
|
||||
self.has_available_quota(&access_token.as_resource_token(), bytes.len() as u64)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Write blob
|
||||
let blob_hash = self
|
||||
.put_blob(account_id, &bytes, false)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.hash;
|
||||
|
||||
// Build node
|
||||
let change_id = self.generate_snowflake_id().caused_by(trc::location!())?;
|
||||
let now = now();
|
||||
let node = FileNode {
|
||||
parent_id,
|
||||
name: resource_name.into_owned(),
|
||||
display_name: None,
|
||||
file: Some(FileProperties {
|
||||
blob_hash,
|
||||
size: bytes.len() as u32,
|
||||
media_type: headers.content_type.map(|v| v.to_string()),
|
||||
executable: false,
|
||||
}),
|
||||
created: now as i64,
|
||||
modified: now as i64,
|
||||
change_id,
|
||||
dead_properties: Default::default(),
|
||||
acls: Default::default(),
|
||||
};
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_change_id(change_id)
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.create_document()
|
||||
.log(LogInsert())
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_changes(node)
|
||||
.with_tenant_id(access_token),
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
self.store()
|
||||
.write(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
|
||||
pub mod calendar;
|
||||
pub mod card;
|
||||
pub mod common;
|
||||
pub mod file;
|
||||
pub mod principal;
|
||||
pub mod request;
|
||||
|
||||
use dav_proto::schema::request::Report;
|
||||
use dav_proto::schema::{request::Report, response::Condition};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::{Method, StatusCode};
|
||||
use jmap_proto::types::collection::Collection;
|
||||
|
||||
pub(crate) type Result<T> = std::result::Result<T, DavError>;
|
||||
|
||||
@@ -47,7 +49,19 @@ pub enum DavMethod {
|
||||
pub(crate) enum DavError {
|
||||
Parse(dav_proto::parser::Error),
|
||||
Internal(trc::Error),
|
||||
UnsupportedReport(Report),
|
||||
Condition(Condition),
|
||||
Code(StatusCode),
|
||||
}
|
||||
|
||||
impl From<DavResource> for Collection {
|
||||
fn from(value: DavResource) -> Self {
|
||||
match value {
|
||||
DavResource::Card => Collection::AddressBook,
|
||||
DavResource::Cal => Collection::Calendar,
|
||||
DavResource::File => Collection::FileNode,
|
||||
DavResource::Principal => Collection::Principal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavResource {
|
||||
@@ -56,7 +70,7 @@ impl DavResource {
|
||||
"card" => DavResource::Card,
|
||||
"cal" => DavResource::Cal,
|
||||
"file" => DavResource::File,
|
||||
"pri" => DavResource::Principal,
|
||||
"pal" => DavResource::Principal,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
parser::{DavParser, tokenizer::Tokenizer},
|
||||
schema::request::{Acl, LockInfo, MkCol, PropFind, PropertyUpdate, Report},
|
||||
schema::{
|
||||
Namespace,
|
||||
request::{Acl, LockInfo, MkCol, PropFind, PropertyUpdate, Report},
|
||||
response::{BaseCondition, ErrorResponse},
|
||||
},
|
||||
};
|
||||
use directory::Permission;
|
||||
use http_proto::{HttpRequest, HttpResponse, HttpSessionData, request::fetch_body};
|
||||
@@ -19,7 +23,7 @@ use hyper::{StatusCode, header};
|
||||
use crate::{
|
||||
DavError, DavMethod, DavResource,
|
||||
file::{
|
||||
UpdateType, acl::FileAclRequestHandler, changes::FileChangesRequestHandler,
|
||||
acl::FileAclRequestHandler, changes::FileChangesRequestHandler,
|
||||
copy_move::FileCopyMoveRequestHandler, delete::FileDeleteRequestHandler,
|
||||
get::FileGetRequestHandler, lock::FileLockRequestHandler, mkcol::FileMkColRequestHandler,
|
||||
propfind::FilePropFindRequestHandler, proppatch::FilePropPatchRequestHandler,
|
||||
@@ -65,6 +69,7 @@ impl DavRequestDispatcher for Server {
|
||||
}
|
||||
|
||||
// Dispatch
|
||||
let todo = "lock tokens, headers, etc";
|
||||
match resource {
|
||||
DavResource::Card => {
|
||||
todo!()
|
||||
@@ -116,16 +121,12 @@ impl DavRequestDispatcher for Server {
|
||||
self.handle_file_delete_request(&access_token, headers)
|
||||
.await
|
||||
}
|
||||
DavMethod::PUT => {
|
||||
self.handle_file_update_request(&access_token, headers, UpdateType::Put(body))
|
||||
.await
|
||||
}
|
||||
DavMethod::POST => {
|
||||
self.handle_file_update_request(&access_token, headers, UpdateType::Post(body))
|
||||
DavMethod::PUT | DavMethod::POST => {
|
||||
self.handle_file_update_request(&access_token, headers, body, false)
|
||||
.await
|
||||
}
|
||||
DavMethod::PATCH => {
|
||||
self.handle_file_update_request(&access_token, headers, UpdateType::Patch(body))
|
||||
self.handle_file_update_request(&access_token, headers, body, true)
|
||||
.await
|
||||
}
|
||||
DavMethod::COPY => {
|
||||
@@ -161,7 +162,7 @@ impl DavRequestDispatcher for Server {
|
||||
self.handle_file_changes_request(&access_token, headers, sync_collection)
|
||||
.await
|
||||
}
|
||||
report => Err(DavError::UnsupportedReport(report)),
|
||||
_ => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
|
||||
},
|
||||
DavMethod::OPTIONS => unreachable!(),
|
||||
},
|
||||
@@ -217,12 +218,36 @@ impl DavRequestHandler for Server {
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(DavError::Internal(err)) => {
|
||||
let is_quota_error = matches!(
|
||||
err.event_type(),
|
||||
trc::EventType::Limit(trc::LimitEvent::Quota | trc::LimitEvent::TenantQuota)
|
||||
);
|
||||
|
||||
trc::error!(err.span_id(session.session_id));
|
||||
|
||||
HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
if is_quota_error {
|
||||
HttpResponse::new(StatusCode::PRECONDITION_FAILED)
|
||||
.with_xml_body(
|
||||
ErrorResponse::new(BaseCondition::QuotaNotExceeded)
|
||||
.with_namespace(resource)
|
||||
.to_string(),
|
||||
)
|
||||
.with_no_cache()
|
||||
} else {
|
||||
HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
Err(DavError::UnsupportedReport(report)) => HttpResponse::new(StatusCode::BAD_REQUEST),
|
||||
Err(DavError::Parse(err)) => HttpResponse::new(StatusCode::BAD_REQUEST),
|
||||
Err(DavError::Condition(condition)) => {
|
||||
HttpResponse::new(StatusCode::PRECONDITION_FAILED)
|
||||
.with_xml_body(
|
||||
ErrorResponse::new(condition)
|
||||
.with_namespace(resource)
|
||||
.to_string(),
|
||||
)
|
||||
.with_no_cache()
|
||||
}
|
||||
Err(DavError::Code(code)) => HttpResponse::new(code),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,3 +263,13 @@ impl From<trc::Error> for DavError {
|
||||
DavError::Internal(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DavResource> for Namespace {
|
||||
fn from(value: DavResource) -> Self {
|
||||
match value {
|
||||
DavResource::Card => Namespace::CardDav,
|
||||
DavResource::Cal => Namespace::CalDav,
|
||||
DavResource::File | DavResource::Principal => Namespace::Dav,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user