/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * 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> + Send; } impl FileCopyMoveRequestHandler for Server { async fn handle_file_copy_move_request( &self, access_token: &AccessToken, headers: &RequestHeaders<'_>, is_move: bool, ) -> crate::Result { // 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::(&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::(); 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::>(); 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, pub document_id: Option, 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, from_resource: UriResource, from_resource_name: &str, destination: Destination, depth: Depth, ) -> crate::Result { 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::>(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::() .caused_by(trc::location!())?; let mut new_node = node.deserialize::().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, from_resource: UriResource, from_resource_name: &str, mut destination: Destination, depth: Depth, delete_source: bool, ) -> crate::Result { 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::>() } else { from_resources .subtree_with_depth(from_resource_name, 1) .map(|r| (r.document_id(), r.hierarchy_seq())) .collect::>() }; // 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::>(ValueKey::archive( from_account_id, Collection::FileNode, document_id, )) .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))? .into_deserialized::() .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, from_resource_path: String, destination: Destination, ) -> crate::Result { 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::>(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::() .caused_by(trc::location!())?; // source_node is the file to be copied let source_node__ = server .store() .get_value::>(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::() .caused_by(trc::location!())?; let mut source_node = source_node_ .deserialize::() .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, destination: Destination, ) -> crate::Result { 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::>(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::() .caused_by(trc::location!())?; // source_node is the file to be copied let mut source_node = server .store() .get_value::>(ValueKey::archive( from_account_id, Collection::FileNode, from_document_id, )) .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))? .deserialize::() .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, from_resource_path: String, destination: Destination, ) -> crate::Result { 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::>(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::() .caused_by(trc::location!())?; let mut new_node = node.deserialize::().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, destination: Destination, ) -> crate::Result { 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::>(ValueKey::archive( from_account_id, Collection::FileNode, from_document_id, )) .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))? .deserialize::() .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, from_resource_path: String, destination: Destination, ) -> crate::Result { let from_account_id = from_resource.account_id; let from_document_id = from_resource.resource.document_id; let node_ = server .store() .get_value::>(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::() .caused_by(trc::location!())?; let mut new_node = node.deserialize::().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, } } }