Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user