245 lines
8.2 KiB
Rust
245 lines
8.2 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
|
|
use dav_proto::RequestHeaders;
|
|
use groupware::file::{FileNode, FileProperties, hierarchy::FileHierarchy};
|
|
use http_proto::HttpResponse;
|
|
use hyper::StatusCode;
|
|
use jmap_proto::types::{
|
|
acl::Acl, collection::Collection, property::Property, type_state::DataType,
|
|
};
|
|
use store::write::{
|
|
Archive, BatchBuilder,
|
|
assert::HashedValue,
|
|
log::{Changes, LogInsert},
|
|
now,
|
|
};
|
|
use trc::AddContext;
|
|
use utils::BlobHash;
|
|
|
|
use crate::{
|
|
DavError,
|
|
common::{acl::DavAclHandler, uri::DavUriResource},
|
|
file::DavFileResource,
|
|
};
|
|
|
|
use super::acl::FileAclRequestHandler;
|
|
|
|
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?;
|
|
let account_id = resource.account_id()?;
|
|
let files = self
|
|
.fetch_file_hierarchy(account_id)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
let resource_name = resource
|
|
.resource
|
|
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
|
|
|
if let Some(document_id) = files.files.by_name(resource_name).map(|r| r.document_id) {
|
|
// Update
|
|
let node_archive_ = self
|
|
.get_property::<HashedValue<Archive>>(
|
|
account_id,
|
|
Collection::FileNode,
|
|
document_id,
|
|
Property::Value,
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
|
let node_archive = node_archive_
|
|
.to_unarchived::<FileNode>()
|
|
.caused_by(trc::location!())?;
|
|
let node = node_archive.inner;
|
|
|
|
// Validate ACL
|
|
self.validate_file_acl(
|
|
access_token,
|
|
account_id,
|
|
node,
|
|
Acl::Modify,
|
|
Acl::ModifyItems,
|
|
)
|
|
.await?;
|
|
|
|
// Verify that the node is a file
|
|
if let Some(file) = node.file.as_ref() {
|
|
if BlobHash::generate(&bytes).as_slice() == file.blob_hash.0.as_slice() {
|
|
return Ok(HttpResponse::new(StatusCode::OK));
|
|
}
|
|
} else {
|
|
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
|
}
|
|
|
|
// Validate quota
|
|
let extra_bytes = (bytes.len() as u64)
|
|
.saturating_sub(u32::from(node.file.as_ref().unwrap().size) as u64);
|
|
if extra_bytes > 0 {
|
|
self.has_available_quota(&access_token.as_resource_token(), extra_bytes)
|
|
.await?;
|
|
}
|
|
|
|
// Write blob
|
|
let blob_hash = self
|
|
.put_blob(account_id, &bytes, false)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.hash;
|
|
|
|
// Build node
|
|
let change_id = self.generate_snowflake_id().caused_by(trc::location!())?;
|
|
let node = node_archive
|
|
.into_deserialized()
|
|
.caused_by(trc::location!())?;
|
|
let mut new_node = node.inner.clone();
|
|
let new_file = new_node.file.as_mut().unwrap();
|
|
new_file.blob_hash = blob_hash;
|
|
new_file.media_type = headers.content_type.map(|v| v.to_string());
|
|
new_file.size = bytes.len() as u32;
|
|
new_node.modified = now() as i64;
|
|
new_node.change_id = change_id;
|
|
|
|
// Prepare write batch
|
|
let mut batch = BatchBuilder::new();
|
|
batch
|
|
.with_change_id(change_id)
|
|
.with_account_id(account_id)
|
|
.with_collection(Collection::FileNode)
|
|
.update_document(document_id)
|
|
.log(Changes::update([document_id]))
|
|
.custom(
|
|
ObjectIndexBuilder::new()
|
|
.with_current(node)
|
|
.with_changes(new_node)
|
|
.with_tenant_id(access_token),
|
|
)
|
|
.caused_by(trc::location!())?;
|
|
self.store()
|
|
.write(batch)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
// Broadcast state change
|
|
self.broadcast_single_state_change(account_id, change_id, DataType::FileNode)
|
|
.await;
|
|
|
|
Ok(HttpResponse::new(StatusCode::OK))
|
|
} else {
|
|
// Insert
|
|
let (parent_id, resource_name) = files.map_parent(resource_name)?;
|
|
|
|
// Validate ACL
|
|
let parent_id = self
|
|
.validate_and_map_parent_acl(
|
|
access_token,
|
|
account_id,
|
|
Collection::FileNode,
|
|
parent_id,
|
|
Acl::AddItems,
|
|
)
|
|
.await?;
|
|
|
|
// Verify that parent is a collection
|
|
if parent_id > 0
|
|
&& self
|
|
.get_property::<Archive>(
|
|
account_id,
|
|
Collection::FileNode,
|
|
parent_id - 1,
|
|
Property::Value,
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
|
|
.unarchive::<FileNode>()
|
|
.caused_by(trc::location!())?
|
|
.file
|
|
.is_some()
|
|
{
|
|
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
|
}
|
|
|
|
// Validate quota
|
|
if !bytes.is_empty() {
|
|
self.has_available_quota(&access_token.as_resource_token(), bytes.len() as u64)
|
|
.await?;
|
|
}
|
|
|
|
// Write blob
|
|
let blob_hash = self
|
|
.put_blob(account_id, &bytes, false)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.hash;
|
|
|
|
// Build node
|
|
let change_id = self.generate_snowflake_id().caused_by(trc::location!())?;
|
|
let now = now();
|
|
let node = FileNode {
|
|
parent_id,
|
|
name: resource_name.into_owned(),
|
|
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,
|
|
change_id,
|
|
dead_properties: Default::default(),
|
|
acls: Default::default(),
|
|
};
|
|
|
|
// Prepare write batch
|
|
let mut batch = BatchBuilder::new();
|
|
batch
|
|
.with_change_id(change_id)
|
|
.with_account_id(account_id)
|
|
.with_collection(Collection::FileNode)
|
|
.create_document()
|
|
.log(LogInsert())
|
|
.custom(
|
|
ObjectIndexBuilder::<(), _>::new()
|
|
.with_changes(node)
|
|
.with_tenant_id(access_token),
|
|
)
|
|
.caused_by(trc::location!())?;
|
|
self.store()
|
|
.write(batch)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
// Broadcast state change
|
|
self.broadcast_single_state_change(account_id, change_id, DataType::FileNode)
|
|
.await;
|
|
|
|
Ok(HttpResponse::new(StatusCode::CREATED))
|
|
}
|
|
}
|
|
}
|