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.
908 lines
29 KiB
Rust
908 lines
29 KiB
Rust
/*
|
|
* 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,
|
|
}
|
|
}
|
|
}
|