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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user