DAV storage methods

This commit is contained in:
mdecimus
2025-03-06 18:35:46 +01:00
parent 3554dea4cb
commit eadd36f4cb
35 changed files with 1289 additions and 206 deletions

View File

@@ -7,12 +7,16 @@ resolver = "2"
[dependencies]
dav-proto = { path = "/Users/me/code/dav-proto" }
common = { path = "../common" }
store = { path = "../store" }
utils = { path = "../utils" }
groupware = { path = "../groupware" }
directory = { path = "../directory" }
http_proto = { path = "../http-proto" }
jmap_proto = { path = "../jmap-proto" }
trc = { path = "../trc" }
hashify = { version = "0.2" }
hyper = { version = "1.0.1", features = ["server", "http1", "http2"] }
percent-encoding = "2.3.1"
[dev-dependencies]

View 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))
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
pub mod acl;
pub mod uri;

View 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))
}
}

View File

@@ -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))
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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))
}
}

View File

@@ -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))
}
}
}

View File

@@ -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
}
}

View File

@@ -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))
}
}
}

View File

@@ -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,
)
}

View File

@@ -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,
}
}
}