/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * 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, is_patch: bool, ) -> impl Future> + Send; } impl FileUpdateRequestHandler for Server { async fn handle_file_update_request( &self, access_token: &AccessToken, headers: &RequestHeaders<'_>, bytes: Vec, _is_patch: bool, ) -> crate::Result { // 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::>(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::() .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::().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)) } } }