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,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