296 lines
10 KiB
Rust
296 lines
10 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* 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 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, 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
|
|
.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.get_resource_token(access_token, account_id).await?,
|
|
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_access_token(access_token),
|
|
)
|
|
.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.get_resource_token(access_token, account_id).await?,
|
|
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_access_token(access_token),
|
|
)
|
|
.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))
|
|
}
|
|
}
|
|
}
|