Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+907
View File
@@ -0,0 +1,907 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::FromDavResource;
use crate::{
DavError, DavMethod,
common::{
ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::{DavUriResource, UriResource},
},
file::{DavFileResource, FileItemId},
};
use common::{
DavResourcePath, DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder,
};
use dav_proto::{Depth, RequestHeaders};
use groupware::{DestroyArchive, cache::GroupwareCache, file::FileNode};
use http_proto::HttpResponse;
use hyper::StatusCode;
use std::sync::Arc;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use store::{
ahash::AHashMap,
write::{BatchBuilder, now},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection, VanishedCollection},
};
pub(crate) trait FileCopyMoveRequestHandler: Sync + Send {
fn handle_file_copy_move_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_move: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl FileCopyMoveRequestHandler for Server {
async fn handle_file_copy_move_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_move: bool,
) -> crate::Result<HttpResponse> {
// Validate source
let from_resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let from_account_id = from_resource_.account_id;
let from_resources = self
.fetch_dav_resources(
access_token.account_id(),
from_account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
let from_resource = from_resources.map_resource::<FileItemId>(&from_resource_)?;
let from_resource_name = from_resource_.resource.unwrap();
// Validate source ACLs
if !access_token.is_member(from_account_id) {
let shared = from_resources.shared_containers(
access_token,
if is_move {
[Acl::Read, Acl::Delete].as_slice().iter().copied()
} else {
[Acl::Read].as_slice().iter().copied()
},
false,
);
for resource in from_resources.subtree(from_resource_.resource.unwrap()) {
if !shared.contains(resource.document_id()) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
}
}
// Validate destination
let destination = self
.validate_uri_with_status(
access_token,
headers
.destination
.ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?,
StatusCode::BAD_GATEWAY,
)
.await?;
if destination.collection != Collection::FileNode {
return Err(DavError::Code(StatusCode::BAD_GATEWAY));
}
let to_account_id = destination
.account_id
.ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?;
let to_resources = if to_account_id == from_account_id {
from_resources.clone()
} else {
self.fetch_dav_resources(
access_token.account_id(),
to_account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?
};
// Map file item
let destination_resource_name = destination
.resource
.ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?;
if from_account_id == to_account_id
&& (from_resource_name == destination_resource_name
|| from_resource_name
.strip_prefix(destination_resource_name)
.is_some_and(|v| v.is_empty() || v.starts_with('/')))
{
return Ok(HttpResponse::new(StatusCode::BAD_GATEWAY));
}
// Check if the resource exists
let mut delete_destination = None;
let mut destination = if let Some((destination, new_name)) =
to_resources.map_parent(destination_resource_name)
{
if let Some(mut existing_destination) = to_resources
.by_path(destination_resource_name)
.map(Destination::from_dav_resource)
{
if !headers.overwrite_fail {
existing_destination.account_id = to_account_id;
delete_destination = Some(existing_destination);
} else {
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED));
}
}
let mut destination = destination
.map(Destination::from_dav_resource)
.unwrap_or_default();
destination.new_name = Some(new_name.to_string());
destination
} else {
return Err(DavError::Code(StatusCode::CONFLICT));
};
destination.account_id = to_account_id;
// Validate destination ACLs
if let Some(document_id) = destination.document_id {
if let Some(delete_destination) = &delete_destination
&& !access_token.is_member(to_account_id)
&& !to_resources.has_access_to_container(
access_token,
delete_destination.document_id.unwrap(),
Acl::Delete,
)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
if !access_token.is_member(to_account_id)
&& !to_resources.has_access_to_container(access_token, document_id, Acl::Modify)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
} else if !access_token.is_member(to_account_id) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![
ResourceState {
account_id: from_account_id,
collection: Collection::FileNode,
document_id: Some(from_resource.resource.document_id),
path: from_resource_name,
..Default::default()
},
ResourceState {
account_id: to_account_id,
collection: Collection::FileNode,
document_id: Some(
delete_destination
.as_ref()
.and_then(|d| d.document_id)
.unwrap_or(u32::MAX),
),
path: destination_resource_name,
..Default::default()
},
],
Default::default(),
if is_move {
DavMethod::MOVE
} else {
DavMethod::COPY
},
)
.await?;
if delete_destination.is_none()
&& from_account_id == destination.account_id
&& from_resource.resource.parent_id == destination.document_id
&& destination.new_name.is_some()
&& is_move
{
// Rename
let from_resource_path = if from_resource.resource.is_container {
from_resources.format_collection(from_resource_name)
} else {
from_resources.format_item(from_resource_name)
};
return rename_item(
self,
access_token,
from_resource,
from_resource_path,
destination,
)
.await;
}
// Validate quota
if !is_move || from_account_id != to_account_id {
let space_needed = from_resources
.subtree(from_resource_name)
.map(|a| a.size() as u64)
.sum::<u64>();
self.has_available_quota(self.account(to_account_id).await?.as_ref(), space_needed)
.await?;
}
// Delete collection
let is_overwrite = delete_destination
.as_ref()
.is_some_and(|d| d.is_container || from_resource.resource.is_container);
if is_overwrite {
delete_destination = None;
// Find ids to delete
let mut ids = to_resources
.subtree(destination_resource_name)
.collect::<Vec<_>>();
if !ids.is_empty() {
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()));
DestroyArchive(sorted_ids)
.delete(
self,
access_token.account_tenant_ids(),
destination.account_id,
None,
)
.await
.caused_by(trc::location!())?;
}
}
match (from_resource.resource.is_container, is_move) {
(true, true) => {
move_container(
self,
access_token,
from_resources,
from_resource,
from_resource_name,
destination,
headers.depth,
)
.await
}
(true, false) => {
copy_container(
self,
access_token,
from_resources,
from_resource,
from_resource_name,
destination,
headers.depth,
false,
)
.await
}
(false, true) => {
if let Some(delete_destination) = delete_destination {
overwrite_and_delete_item(
self,
access_token,
from_resource,
from_resources.format_item(from_resource_name),
delete_destination,
)
.await
} else {
move_item(
self,
access_token,
from_resource,
from_resources.format_item(from_resource_name),
destination,
)
.await
}
}
(false, false) => {
if let Some(delete_destination) = delete_destination {
overwrite_item(self, access_token, from_resource, delete_destination).await
} else {
copy_item(self, access_token, from_resource, destination).await
}
}
}
.map(|r| {
if is_overwrite && r.status() == StatusCode::CREATED {
r.with_status_code(StatusCode::NO_CONTENT)
} else {
r
}
})
}
}
#[derive(Debug)]
pub(crate) struct Destination {
pub account_id: u32,
pub new_name: Option<String>,
pub document_id: Option<u32>,
pub is_container: bool,
}
impl Default for Destination {
fn default() -> Self {
Self {
account_id: Default::default(),
document_id: Default::default(),
new_name: Default::default(),
is_container: true,
}
}
}
// Moves a container under an existing container
async fn move_container(
server: &Server,
access_token: &AccessToken,
from_resources: Arc<DavResources>,
from_resource: UriResource<u32, FileItemId>,
from_resource_name: &str,
destination: Destination,
depth: Depth,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let from_document_id = from_resource.resource.document_id;
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
if from_account_id == to_account_id {
let node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
new_node.parent_id = parent_id;
if let Some(new_name) = destination.new_name {
new_node.name = new_name;
}
let mut batch = BatchBuilder::new();
let etag = new_node
.update(
access_token.account_tenant_ids(),
node,
from_account_id,
from_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
batch.with_account_id(from_account_id).log_vanished_item(
VanishedCollection::FileNode,
from_resources.format_collection(from_resource_name),
);
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
} else {
copy_container(
server,
access_token,
from_resources,
from_resource,
from_resource_name,
destination,
depth,
true,
)
.await
}
}
#[allow(clippy::too_many_arguments)]
async fn copy_container(
server: &Server,
access_token: &AccessToken,
from_resources: Arc<DavResources>,
from_resource: UriResource<u32, FileItemId>,
from_resource_name: &str,
mut destination: Destination,
depth: Depth,
delete_source: bool,
) -> crate::Result<HttpResponse> {
let infinity_copy = match depth {
Depth::Zero if !delete_source => {
return copy_item(server, access_token, from_resource, destination).await;
}
Depth::One if !delete_source => false,
_ => true,
};
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
// Obtain files to copy
let mut copy_files = if infinity_copy {
from_resources
.subtree(from_resource_name)
.map(|r| (r.document_id(), r.hierarchy_seq()))
.collect::<Vec<_>>()
} else {
from_resources
.subtree_with_depth(from_resource_name, 1)
.map(|r| (r.document_id(), r.hierarchy_seq()))
.collect::<Vec<_>>()
};
// Top-down copy
let mut batch = BatchBuilder::new();
let mut id_map = AHashMap::with_capacity(copy_files.len());
let mut delete_files = if delete_source {
Vec::with_capacity(copy_files.len())
} else {
Vec::new()
};
copy_files.sort_unstable_by_key(|a| a.1);
let now = now() as i64;
let mut next_document_id = server
.store()
.assign_document_ids(to_account_id, Collection::FileNode, copy_files.len() as u64)
.await
.caused_by(trc::location!())?;
for (document_id, _) in copy_files.into_iter() {
let node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
.into_deserialized::<FileNode>()
.caused_by(trc::location!())?;
// Build node
let mut node = if !delete_source {
node_.inner
} else {
let node = node_.inner.clone();
delete_files.push((document_id, node_));
node
};
node.modified = now;
node.created = now;
if let Some(new_name) = destination.new_name.take() {
node.name = new_name;
}
node.parent_id = if let Some(&prev_document_id) = id_map.get(&node.parent_id) {
prev_document_id
} else {
parent_id
};
// Prepare write batch
let new_document_id = next_document_id;
next_document_id -= 1;
batch
.with_account_id(to_account_id)
.with_collection(Collection::FileNode)
.with_document(new_document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(node)
.with_changed_by(access_token.account_tenant_ids()),
)
.caused_by(trc::location!())?
.commit_point();
id_map.insert(document_id + 1, new_document_id + 1);
}
// Delete nodes
if !delete_files.is_empty() {
for (document_id, node) in delete_files.into_iter().rev() {
// Delete record
batch
.with_account_id(from_account_id)
.with_collection(Collection::FileNode)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_changed_by(access_token.account_tenant_ids())
.with_current(node),
)
.caused_by(trc::location!())?
.commit_point();
}
batch.with_account_id(from_account_id).log_vanished_item(
VanishedCollection::FileNode,
from_resources.format_collection(from_resource_name),
);
}
// Write changes
if !batch.is_empty() {
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
}
Ok(HttpResponse::new(StatusCode::CREATED))
}
// Overwrites the contents of one file with another, then deletes the original
async fn overwrite_and_delete_item(
server: &Server,
access_token: &AccessToken,
from_resource: UriResource<u32, FileItemId>,
from_resource_path: String,
destination: Destination,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let from_document_id = from_resource.resource.document_id;
let to_document_id = destination.document_id.unwrap();
// dest_node is the current file at the destination
let dest_node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
to_account_id,
Collection::FileNode,
to_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let dest_node = dest_node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
// source_node is the file to be copied
let source_node__ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let source_node_ = source_node__
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut source_node = source_node_
.deserialize::<FileNode>()
.caused_by(trc::location!())?;
source_node.name = if let Some(new_name) = destination.new_name {
new_name
} else {
dest_node.inner.name.to_string()
};
source_node.parent_id = dest_node.inner.parent_id.into();
let mut batch = BatchBuilder::new();
let etag = source_node
.update(
access_token.account_tenant_ids(),
dest_node,
to_account_id,
to_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
DestroyArchive(source_node_)
.delete(
access_token.account_tenant_ids(),
from_account_id,
from_document_id,
&mut batch,
from_resource_path,
)
.caused_by(trc::location!())?;
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
}
// Overwrites the contents of one file with another
async fn overwrite_item(
server: &Server,
access_token: &AccessToken,
from_resource: UriResource<u32, FileItemId>,
destination: Destination,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let from_document_id = from_resource.resource.document_id;
let to_document_id = destination.document_id.unwrap();
// dest_node is the current file at the destination
let dest_node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
to_account_id,
Collection::FileNode,
to_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let dest_node = dest_node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
// source_node is the file to be copied
let mut source_node = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
.deserialize::<FileNode>()
.caused_by(trc::location!())?;
source_node.name = if let Some(new_name) = destination.new_name {
new_name
} else {
dest_node.inner.name.to_string()
};
source_node.parent_id = dest_node.inner.parent_id.into();
let mut batch = BatchBuilder::new();
let etag = source_node
.update(
access_token.account_tenant_ids(),
dest_node,
to_account_id,
to_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
}
// Moves an item under an existing container
async fn move_item(
server: &Server,
access_token: &AccessToken,
from_resource: UriResource<u32, FileItemId>,
from_resource_path: String,
destination: Destination,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let from_document_id = from_resource.resource.document_id;
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
let node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
new_node.parent_id = parent_id;
if let Some(new_name) = destination.new_name {
new_node.name = new_name;
}
let mut batch = BatchBuilder::new();
let etag = if from_account_id == to_account_id {
// Destination is in the same account: just update the parent id
batch.log_vanished_item(VanishedCollection::FileNode, from_resource_path);
new_node
.update(
access_token.account_tenant_ids(),
node,
from_account_id,
from_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag()
} else {
// Destination is in a different account: insert a new node, then delete the old one
let to_document_id = server
.store()
.assign_document_ids(to_account_id, Collection::FileNode, 1)
.await
.caused_by(trc::location!())?;
let etag = new_node
.insert(
access_token.account_tenant_ids(),
to_account_id,
to_document_id,
true,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
DestroyArchive(node)
.delete(
access_token.account_tenant_ids(),
from_account_id,
from_document_id,
&mut batch,
from_resource_path,
)
.caused_by(trc::location!())?;
etag
};
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
// Copies an item under an existing container
async fn copy_item(
server: &Server,
access_token: &AccessToken,
from_resource: UriResource<u32, FileItemId>,
destination: Destination,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let from_document_id = from_resource.resource.document_id;
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
let mut node = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
.deserialize::<FileNode>()
.caused_by(trc::location!())?;
node.parent_id = parent_id;
if let Some(new_name) = destination.new_name {
node.name = new_name;
}
let mut batch = BatchBuilder::new();
let to_document_id = server
.store()
.assign_document_ids(to_account_id, Collection::FileNode, 1)
.await
.caused_by(trc::location!())?;
let etag = node
.insert(
access_token.account_tenant_ids(),
to_account_id,
to_document_id,
true,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
// Renames an item
async fn rename_item(
server: &Server,
access_token: &AccessToken,
from_resource: UriResource<u32, FileItemId>,
from_resource_path: String,
destination: Destination,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let from_document_id = from_resource.resource.document_id;
let node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
if let Some(new_name) = destination.new_name {
new_node.name = new_name;
}
let mut batch = BatchBuilder::new();
let etag = new_node
.update(
access_token.account_tenant_ids(),
node,
from_account_id,
from_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
batch.log_vanished_item(VanishedCollection::FileNode, from_resource_path);
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
impl FromDavResource for Destination {
fn from_dav_resource(item: DavResourcePath<'_>) -> Self {
Destination {
account_id: u32::MAX,
document_id: Some(item.document_id()),
is_container: item.is_container(),
new_name: None,
}
}
}
+107
View File
@@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken};
use dav_proto::RequestHeaders;
use groupware::{DestroyArchive, cache::GroupwareCache};
use http_proto::HttpResponse;
use hyper::StatusCode;
use trc::AddContext;
use types::{acl::Acl, collection::SyncCollection};
pub(crate) trait FileDeleteRequestHandler: Sync + Send {
fn handle_file_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl FileDeleteRequestHandler for Server {
async fn handle_file_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let delete_path = resource
.resource
.filter(|r| !r.is_empty())
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
// Find ids to delete
let mut ids = resources.subtree(delete_path).collect::<Vec<_>>();
if ids.is_empty() {
return Err(DavError::Code(StatusCode::NOT_FOUND));
}
// Sort ids descending from the deepest to the root
ids.sort_unstable_by_key(|b| std::cmp::Reverse(b.hierarchy_seq()));
let (document_id, full_delete_path) = ids
.last()
.map(|a| (a.document_id(), resources.format_resource(*a)))
.unwrap();
let mut sorted_ids = Vec::with_capacity(ids.len());
sorted_ids.extend(ids.into_iter().map(|a| a.document_id()));
// Validate ACLs
if !access_token.is_member(account_id) {
let permissions = resources.shared_containers(access_token, [Acl::Delete], false);
if permissions.len() < sorted_ids.len() as u64
|| !sorted_ids.iter().all(|id| permissions.contains(*id))
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: document_id.into(),
path: delete_path,
..Default::default()
}],
Default::default(),
DavMethod::DELETE,
)
.await?;
DestroyArchive(sorted_ids)
.delete(
self,
access_token.account_tenant_ids(),
account_id,
full_delete_path.into(),
)
.await?;
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
}
}
+168
View File
@@ -0,0 +1,168 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
ETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
};
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime};
use groupware::{cache::GroupwareCache, file::FileNode};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::{
ValueKey,
write::{AlignedBytes, Archive, now},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait FileGetRequestHandler: Sync + Send {
fn handle_file_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl FileGetRequestHandler for Server {
async fn handle_file_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let files = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
let resource = files.map_resource(&resource_)?;
// Fetch node
let node_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::FileNode,
resource.resource,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_.unarchive::<FileNode>().caused_by(trc::location!())?;
// Validate ACL
if !access_token.is_member(account_id)
&& !node.acls.effective_acl(access_token).contains(Acl::Read)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
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));
};
// Validate headers
let etag = node_.etag();
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: resource.resource.into(),
etag: etag.clone().into(),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::GET,
)
.await?;
let modified = i64::from(node.modified);
let last_modified = Rfc1123DateTime::new(modified).to_string();
let byte_range = if !is_head && size > 0 {
headers
.range
.filter(|_| {
headers.eval_if_range(
&etag,
((modified as u64) < now()).then_some(last_modified.as_str()),
)
})
.map(|range| range.resolve(size as u64))
} else {
None
};
let byte_range = match byte_range {
Some(Some(range)) => Some(range.start as usize..range.end as usize),
Some(None) => {
return Ok(HttpResponse::new(StatusCode::RANGE_NOT_SATISFIABLE)
.with_accept_ranges()
.with_etag(etag)
.with_content_range(format!("bytes */{size}")));
}
None => None,
};
let response = HttpResponse::new(StatusCode::OK)
.with_content_type(content_type.unwrap_or("application/octet-stream"))
.with_etag(etag)
.with_last_modified(last_modified)
.with_accept_ranges();
if is_head {
return Ok(response.with_content_length(size));
}
let contents = self
.blob_store()
.get_blob(hash, byte_range.clone().unwrap_or(0..usize::MAX))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
Ok(match byte_range {
Some(byte_range) if !contents.is_empty() => response
.with_status_code(StatusCode::PARTIAL_CONTENT)
.with_content_range(format!(
"bytes {}-{}/{}",
byte_range.start,
byte_range.start + contents.len() - 1,
size
)),
Some(_) => return Err(DavError::Code(StatusCode::NOT_FOUND)),
None => response,
}
.with_binary_body(contents))
}
}
+146
View File
@@ -0,0 +1,146 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::proppatch::FilePropPatchRequestHandler;
use crate::{
DavMethod, PropStatBuilder,
common::{
ExtractETag,
acl::ResourceAcl,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
};
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use dav_proto::{
RequestHeaders, Return,
schema::{Namespace, request::MkCol, response::MkColResponse},
};
use groupware::{cache::GroupwareCache, file::FileNode};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::{BatchBuilder, now};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait FileMkColRequestHandler: Sync + Send {
fn handle_file_mkcol_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: Option<MkCol>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl FileMkColRequestHandler for Server {
async fn handle_file_mkcol_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: Option<MkCol>,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
let resource = resources.map_parent_resource(&resource_)?;
// Validate and map parent ACL
let parent_id = resources.validate_and_map_parent_acl(
access_token,
access_token.is_member(account_id),
resource.resource.0,
Acl::AddItems,
)?;
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(u32::MAX),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::MKCOL,
)
.await?;
// Build file container
let now = now();
let mut node = FileNode {
parent_id,
name: resource.resource.1.to_string(),
display_name: None,
file: None,
created: now as i64,
modified: now as i64,
dead_properties: Default::default(),
acls: Default::default(),
};
// Apply MKCOL properties
let mut return_prop_stat = None;
if let Some(mkcol) = request {
let mut prop_stat = PropStatBuilder::default();
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.build())
.with_namespace(Namespace::Dav)
.to_string(),
));
}
if headers.ret != Return::Minimal {
return_prop_stat = Some(prop_stat);
}
}
// Prepare write batch
let document_id = self
.store()
.assign_document_ids(account_id, Collection::FileNode, 1)
.await
.caused_by(trc::location!())?;
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.with_document(document_id)
.custom(ObjectIndexBuilder::<(), _>::new().with_changes(node))
.caused_by(trc::location!())?;
let etag = batch.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
if let Some(prop_stat) = return_prop_stat {
Ok(HttpResponse::new(StatusCode::CREATED)
.with_xml_body(
MkColResponse::new(prop_stat.build())
.with_namespace(Namespace::Dav)
.to_string(),
)
.with_etag_opt(etag))
} else {
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
}
}
+153
View File
@@ -0,0 +1,153 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError,
common::uri::{OwnedUri, UriResource},
};
use common::{DavResourcePath, DavResources};
use dav_proto::schema::property::{DavProperty, WebDavProperty};
use hyper::StatusCode;
pub mod copy_move;
pub mod delete;
pub mod get;
pub mod mkcol;
pub mod proppatch;
pub mod update;
pub(crate) static FILE_CONTAINER_PROPS: [DavProperty; 19] = [
DavProperty::WebDav(WebDavProperty::CreationDate),
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::GetETag),
DavProperty::WebDav(WebDavProperty::GetLastModified),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::LockDiscovery),
DavProperty::WebDav(WebDavProperty::SupportedLock),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::SyncToken),
DavProperty::WebDav(WebDavProperty::Owner),
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
DavProperty::WebDav(WebDavProperty::Acl),
DavProperty::WebDav(WebDavProperty::AclRestrictions),
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes),
DavProperty::WebDav(WebDavProperty::QuotaUsedBytes),
];
pub(crate) static FILE_ITEM_PROPS: [DavProperty; 19] = [
DavProperty::WebDav(WebDavProperty::CreationDate),
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::GetETag),
DavProperty::WebDav(WebDavProperty::GetLastModified),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::LockDiscovery),
DavProperty::WebDav(WebDavProperty::SupportedLock),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::SyncToken),
DavProperty::WebDav(WebDavProperty::Owner),
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
DavProperty::WebDav(WebDavProperty::Acl),
DavProperty::WebDav(WebDavProperty::AclRestrictions),
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
DavProperty::WebDav(WebDavProperty::GetContentLanguage),
DavProperty::WebDav(WebDavProperty::GetContentLength),
DavProperty::WebDav(WebDavProperty::GetContentType),
];
pub(crate) trait FromDavResource {
fn from_dav_resource(item: DavResourcePath<'_>) -> Self;
}
pub(crate) struct FileItemId {
pub document_id: u32,
pub parent_id: Option<u32>,
pub is_container: bool,
}
pub(crate) trait DavFileResource {
fn map_resource<T: FromDavResource>(
&self,
resource: &OwnedUri<'_>,
) -> crate::Result<UriResource<u32, T>>;
fn map_parent<'x>(&self, resource: &'x str) -> Option<(Option<DavResourcePath<'_>>, &'x str)>;
#[allow(clippy::type_complexity)]
fn map_parent_resource<'x, T: FromDavResource>(
&self,
resource: &OwnedUri<'x>,
) -> crate::Result<UriResource<u32, (Option<T>, &'x str)>>;
}
impl DavFileResource for DavResources {
fn map_resource<T: FromDavResource>(
&self,
resource: &OwnedUri<'_>,
) -> crate::Result<UriResource<u32, T>> {
resource
.resource
.and_then(|r| self.by_path(r))
.map(|r| UriResource {
collection: resource.collection,
account_id: resource.account_id,
resource: T::from_dav_resource(r),
})
.ok_or(DavError::Code(StatusCode::NOT_FOUND))
}
fn map_parent<'x>(&self, resource: &'x str) -> Option<(Option<DavResourcePath<'_>>, &'x str)> {
let (parent, child) = if let Some((parent, child)) = resource.rsplit_once('/') {
(Some(self.by_path(parent)?), child)
} else {
(None, resource)
};
Some((parent, child))
}
fn map_parent_resource<'x, T: FromDavResource>(
&self,
resource: &OwnedUri<'x>,
) -> crate::Result<UriResource<u32, (Option<T>, &'x str)>> {
if let Some(r) = resource.resource {
if self.by_path(r).is_none() {
self.map_parent(r)
.map(|(parent, child)| UriResource {
collection: resource.collection,
account_id: resource.account_id,
resource: (parent.map(T::from_dav_resource), child),
})
.ok_or(DavError::Code(StatusCode::CONFLICT))
} else {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
} else {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
}
impl FromDavResource for u32 {
fn from_dav_resource(item: DavResourcePath) -> Self {
item.document_id()
}
}
impl FromDavResource for FileItemId {
fn from_dav_resource(item: DavResourcePath) -> Self {
FileItemId {
document_id: item.document_id(),
parent_id: item.parent_id(),
is_container: item.is_container(),
}
}
}
+304
View File
@@ -0,0 +1,304 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod, PropStatBuilder,
common::{
ETag, ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
};
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
use dav_proto::{
RequestHeaders, Return,
schema::{
property::{DavProperty, DavValue, ResourceType, WebDavProperty},
request::{DavPropertyValue, PropertyUpdate},
response::{BaseCondition, MultiStatus, Response},
},
};
use groupware::{cache::GroupwareCache, file::FileNode};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::BatchBuilder;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait FilePropPatchRequestHandler: Sync + Send {
fn handle_file_proppatch_request(
&self,
access_token: &AccessToken,
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 PropStatBuilder,
) -> bool;
}
impl FilePropPatchRequestHandler for Server {
async fn handle_file_proppatch_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
mut request: PropertyUpdate,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let uri = headers.uri;
let account_id = resource_.account_id;
let files = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
let resource = files.map_resource(&resource_)?;
if !request.has_changes() {
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
}
// Fetch node
let node_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::FileNode,
resource.resource,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
// Validate ACL
if !access_token.is_member(account_id)
&& !node
.inner
.acls
.effective_acl(access_token)
.contains(Acl::Modify)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: resource.resource.into(),
etag: node_.etag().into(),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::PROPPATCH,
)
.await?;
// Deserialize
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
// Remove properties
let mut items = PropStatBuilder::default();
if !request.set_first && !request.remove.is_empty() {
remove_file_properties(
&mut new_node,
std::mem::take(&mut request.remove),
&mut items,
);
}
// Set properties
let is_success = self.apply_file_properties(&mut new_node, true, request.set, &mut items);
// Remove properties
if is_success && !request.remove.is_empty() {
remove_file_properties(&mut new_node, request.remove, &mut items);
}
let etag = if is_success {
let mut batch = BatchBuilder::new();
let etag = new_node
.update(
access_token.account_tenant_ids(),
node,
account_id,
resource.resource,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
etag
} else {
node_.etag().into()
};
if headers.ret != Return::Minimal || !is_success {
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
.with_xml_body(
MultiStatus::new(vec![Response::new_propstat(uri, items.build())]).to_string(),
)
.with_etag_opt(etag))
} else {
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
}
}
fn apply_file_properties(
&self,
file: &mut FileNode,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> 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.groupware.live_property_size {
file.display_name = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
file.created = dt;
items.insert_ok(property.property);
}
(DavProperty::WebDav(WebDavProperty::GetContentType), DavValue::String(name))
if file.file.is_some() =>
{
if name.len() <= self.core.groupware.live_property_size {
file.file.as_mut().unwrap().media_type = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value 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.insert_precondition_failed(
property.property,
StatusCode::FORBIDDEN,
BaseCondition::ValidResourceType,
);
has_errors = true;
} else {
items.insert_ok(property.property);
}
}
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
if self.core.groupware.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.groupware.dead_property_size.unwrap()
{
file.dead_properties.add_element(dead.clone(), values.0);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(_, DavValue::Null) => {
items.insert_ok(property.property);
}
_ => {
items.insert_error_with_description(
property.property,
StatusCode::CONFLICT,
"Property cannot be modified",
);
has_errors = true;
}
}
}
!has_errors
}
}
fn remove_file_properties(
node: &mut FileNode,
properties: Vec<DavProperty>,
items: &mut PropStatBuilder,
) {
for property in properties {
match &property {
DavProperty::WebDav(WebDavProperty::DisplayName) => {
node.display_name = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::WebDav(WebDavProperty::GetContentType) if node.file.is_some() => {
node.file.as_mut().unwrap().media_type = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::DeadProperty(dead) => {
node.dead_properties.remove_element(dead);
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
_ => {
items.insert_error_with_description(
property,
StatusCode::CONFLICT,
"Property cannot be deleted",
);
}
}
}
}
+305
View File
@@ -0,0 +1,305 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
ETag, ExtractETag,
acl::ResourceAcl,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
};
use common::{
Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder,
};
use dav_proto::{RequestHeaders, Return, schema::property::Rfc1123DateTime};
use groupware::{
cache::GroupwareCache,
file::{FileNode, FileProperties},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::{BatchBuilder, now};
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
blob_hash::BlobHash,
collection::{Collection, SyncCollection},
};
pub(crate) trait FileUpdateRequestHandler: Sync + Send {
fn handle_file_update_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
is_patch: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl FileUpdateRequestHandler for Server {
async fn handle_file_update_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
_is_patch: bool,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
let resource_name = resource
.resource
.ok_or(DavError::Code(StatusCode::CONFLICT))?;
if bytes.len() > self.core.groupware.max_file_size {
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
}
if let Some(document_id) = resources
.by_path(resource_name.as_ref())
.map(|r| r.document_id())
{
// Update
let node_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::FileNode,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
// Validate ACL
if !access_token.is_member(account_id)
&& !node
.inner
.acls
.effective_acl(access_token)
.contains(Acl::Modify)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
match self
.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(document_id),
etag: node.etag().into(),
path: resource_name,
..Default::default()
}],
Default::default(),
DavMethod::PUT,
)
.await
{
Ok(_) => {}
Err(DavError::Code(StatusCode::PRECONDITION_FAILED))
if headers.ret == Return::Representation =>
{
let file = node.inner.file.as_ref().unwrap();
let contents = self
.blob_store()
.get_blob(file.blob_hash.0.as_slice(), 0..usize::MAX)
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::PRECONDITION_FAILED))?;
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)
.with_content_type(
file.media_type
.as_ref()
.map(|v| v.as_str())
.unwrap_or("application/octet-stream"),
)
.with_etag(node.etag())
.with_last_modified(
Rfc1123DateTime::new(i64::from(node.inner.modified)).to_string(),
)
.with_header("Preference-Applied", "return=representation")
.with_binary_body(contents));
}
Err(e) => return Err(e),
}
// Verify that the node is a file
if let Some(file) = node.inner.file.as_ref() {
if BlobHash::generate(&bytes).as_slice() == file.blob_hash.0.as_slice() {
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
}
} else {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate quota
let extra_bytes = (bytes.len() as u64)
.saturating_sub(u32::from(node.inner.file.as_ref().unwrap().size) as u64);
if extra_bytes > 0 {
self.has_available_quota(self.account(account_id).await?.as_ref(), extra_bytes)
.await?;
}
// Write blob
let (blob_hash, blob_hold) = self
.put_temporary_blob(account_id, &bytes, 60)
.await
.caused_by(trc::location!())?;
// Build node
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
let new_file = new_node.file.as_mut().unwrap();
new_file.blob_hash = blob_hash;
new_file.media_type = headers
.content_type
.filter(|ct| !ct.is_empty() && *ct != "application/octet-stream")
.map(|v| v.to_string());
new_file.size = bytes.len() as u32;
new_node.modified = now() as i64;
// Prepare write batch
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.with_document(document_id)
.clear(blob_hold)
.custom(
ObjectIndexBuilder::new()
.with_current(node)
.with_changes(new_node)
.with_changed_by(access_token.account_tenant_ids()),
)
.caused_by(trc::location!())?;
let etag = batch.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
} else {
// Insert
let orig_resource_name = resource_name;
let (parent, resource_name) = resources
.map_parent(orig_resource_name.as_ref())
.ok_or(DavError::Code(StatusCode::CONFLICT))?;
// Validate ACL
let parent_id = resources.validate_and_map_parent_acl(
access_token,
access_token.is_member(account_id),
parent.map(|r| r.document_id()),
Acl::AddItems,
)?;
// Verify that parent is a collection
if parent.as_ref().is_some_and(|r| !r.is_container()) {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(u32::MAX),
path: orig_resource_name,
..Default::default()
}],
Default::default(),
DavMethod::PUT,
)
.await?;
// Validate quota
if !bytes.is_empty() {
self.has_available_quota(
self.account(account_id).await?.as_ref(),
bytes.len() as u64,
)
.await?;
}
// Write blob
let (blob_hash, blob_hold) = self
.put_temporary_blob(account_id, &bytes, 60)
.await
.caused_by(trc::location!())?;
// Build node
let now = now();
let node = FileNode {
parent_id,
name: resource_name.to_string(),
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,
dead_properties: Default::default(),
acls: parent
.as_ref()
.and_then(|p| p.resource.acls())
.map(|acls| acls.to_vec())
.unwrap_or_default(),
};
// Prepare write batch
let mut batch = BatchBuilder::new();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::FileNode, 1)
.await
.caused_by(trc::location!())?;
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.with_document(document_id)
.clear(blob_hold)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(node)
.with_changed_by(access_token.account_tenant_ids()),
)
.caused_by(trc::location!())?;
let etag = batch.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
}
}