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,112 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::download::BlobDownload;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use jmap_proto::{
|
||||
error::set::{SetError, SetErrorType},
|
||||
method::copy::{CopyBlobRequest, CopyBlobResponse},
|
||||
request::MaybeInvalid,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::future::Future;
|
||||
use store::write::{BatchBuilder, BlobLink, BlobOp, now};
|
||||
use trc::AddContext;
|
||||
use types::blob::{BlobClass, BlobId};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub trait BlobCopy: Sync + Send {
|
||||
fn blob_copy(
|
||||
&self,
|
||||
request: CopyBlobRequest,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<CopyBlobResponse>> + Send;
|
||||
}
|
||||
|
||||
impl BlobCopy for Server {
|
||||
async fn blob_copy(
|
||||
&self,
|
||||
request: CopyBlobRequest,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<CopyBlobResponse> {
|
||||
let mut response = CopyBlobResponse {
|
||||
from_account_id: request.from_account_id,
|
||||
account_id: request.account_id,
|
||||
copied: VecMap::with_capacity(request.blob_ids.len()),
|
||||
not_copied: VecMap::new(),
|
||||
};
|
||||
let account_id = request.account_id.document_id();
|
||||
|
||||
for blob_id in request.blob_ids {
|
||||
let blob_id = match blob_id {
|
||||
MaybeInvalid::Value(blob_id) => blob_id,
|
||||
invalid => {
|
||||
response.not_copied.append(
|
||||
invalid,
|
||||
SetError::new(SetErrorType::BlobNotFound).with_description(
|
||||
"blobId does not exist or not enough permissions to access it.",
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if self.has_access_blob(&blob_id, access_token).await? {
|
||||
// Enforce quota
|
||||
if !access_token.has_permission(Permission::UnlimitedUploads)
|
||||
&& !self
|
||||
.blob_has_quota(account_id, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.allowed
|
||||
{
|
||||
response.not_copied.append(
|
||||
blob_id,
|
||||
SetError::over_quota().with_description(format!(
|
||||
"You have exceeded the blob quota of {} files or {} bytes.",
|
||||
self.core.jmap.upload_tmp_quota_amount,
|
||||
self.core.jmap.upload_tmp_quota_size
|
||||
)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
let until = now() + self.core.jmap.upload_tmp_ttl;
|
||||
batch.with_account_id(account_id).set(
|
||||
BlobOp::Link {
|
||||
hash: blob_id.hash.clone(),
|
||||
to: BlobLink::Temporary { until },
|
||||
},
|
||||
vec![],
|
||||
);
|
||||
self.store()
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let dest_blob_id = BlobId {
|
||||
hash: blob_id.hash.clone(),
|
||||
class: BlobClass::Reserved {
|
||||
account_id,
|
||||
expires: until,
|
||||
},
|
||||
section: blob_id.section.clone(),
|
||||
};
|
||||
|
||||
response.copied.append(blob_id, dest_blob_id);
|
||||
} else {
|
||||
response.not_copied.append(
|
||||
blob_id,
|
||||
SetError::new(SetErrorType::BlobNotFound).with_description(
|
||||
"blobId does not exist or not enough permissions to access it.",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use email::cache::MessageCacheFetch;
|
||||
use email::cache::email::MessageCacheAccess;
|
||||
use email::message::metadata::MessageMetadata;
|
||||
use groupware::cache::GroupwareCache;
|
||||
use registry::schema::enums::Permission;
|
||||
use std::future::Future;
|
||||
use store::ValueKey;
|
||||
use store::write::{AlignedBytes, Archive};
|
||||
use trc::AddContext;
|
||||
use types::acl::Acl;
|
||||
use types::blob::{BlobClass, BlobId};
|
||||
use types::collection::{Collection, SyncCollection};
|
||||
use types::field::EmailField;
|
||||
use utils::chained_bytes::ChainedBytes;
|
||||
|
||||
pub trait BlobDownload: Sync + Send {
|
||||
fn blob_download(
|
||||
&self,
|
||||
blob_id: &BlobId,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<Option<Vec<u8>>>> + Send;
|
||||
|
||||
fn has_access_blob(
|
||||
&self,
|
||||
blob_id: &BlobId,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<bool>> + Send;
|
||||
}
|
||||
|
||||
impl BlobDownload for Server {
|
||||
#[allow(clippy::blocks_in_conditions)]
|
||||
async fn blob_download(
|
||||
&self,
|
||||
blob_id: &BlobId,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<Option<Vec<u8>>> {
|
||||
if self.has_access_blob(blob_id, access_token).await? {
|
||||
if let Some(section) = &blob_id.section {
|
||||
self.get_blob_section(&blob_id.hash, section)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
} else {
|
||||
let blob = self
|
||||
.blob_store()
|
||||
.get_blob(blob_id.hash.as_slice(), 0..usize::MAX)
|
||||
.await
|
||||
.caused_by(trc::location!());
|
||||
match (&blob_id.class, blob) {
|
||||
(
|
||||
BlobClass::Linked {
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
},
|
||||
Ok(Some(data)),
|
||||
) if *collection == Collection::Email as u8 => {
|
||||
let Some(archive) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::property(
|
||||
*account_id,
|
||||
Collection::Email,
|
||||
*document_id,
|
||||
EmailField::Metadata,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
return Ok(Some(data));
|
||||
};
|
||||
let metadata = archive
|
||||
.to_unarchived::<MessageMetadata>()
|
||||
.caused_by(trc::location!())?;
|
||||
let body_offset = metadata.inner.blob_body_offset.to_native();
|
||||
if metadata.inner.root_part().offset_body.to_native() != body_offset {
|
||||
let raw_message = ChainedBytes::new(
|
||||
metadata.inner.raw_headers.as_ref(),
|
||||
)
|
||||
.with_last(data.get(body_offset as usize..).unwrap_or_default());
|
||||
Ok(Some(raw_message.to_bytes()))
|
||||
} else {
|
||||
Ok(Some(data))
|
||||
}
|
||||
}
|
||||
(_, blob) => blob,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn has_access_blob(
|
||||
&self,
|
||||
blob_id: &BlobId,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<bool> {
|
||||
Ok(
|
||||
(blob_id.class.is_superuser() && access_token.has_permission(Permission::FetchAnyBlob))
|
||||
|| (self
|
||||
.store()
|
||||
.blob_has_access(&blob_id.hash, &blob_id.class)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
&& match &blob_id.class {
|
||||
BlobClass::Linked {
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
} => {
|
||||
if access_token.is_member(*account_id) {
|
||||
true
|
||||
} else {
|
||||
match Collection::from(*collection) {
|
||||
Collection::Email => self
|
||||
.get_cached_messages(*account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.shared_messages(access_token, Acl::ReadItems)
|
||||
.contains(*document_id),
|
||||
collection @ (Collection::FileNode
|
||||
| Collection::ContactCard
|
||||
| Collection::CalendarEvent) => self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
*account_id,
|
||||
SyncCollection::from(collection),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.shared_items(access_token, [Acl::ReadItems], true)
|
||||
.contains(*document_id),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
BlobClass::Reserved { account_id, .. } => {
|
||||
access_token.is_member(*account_id)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::download::BlobDownload;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use email::message::metadata::MessageData;
|
||||
use jmap_proto::{
|
||||
method::{
|
||||
get::{GetRequest, GetResponse},
|
||||
lookup::{BlobInfo, BlobLookupRequest, BlobLookupResponse},
|
||||
},
|
||||
object::blob::{Blob, BlobProperty, BlobValue, DataProperty, DigestProperty},
|
||||
request::{IntoValid, MaybeInvalid},
|
||||
};
|
||||
use jmap_tools::{Map, Value};
|
||||
use mail_builder::encoders::Base64Encoder;
|
||||
use sha1::{Digest, Sha1};
|
||||
use sha2::{Sha256, Sha512};
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{blob::BlobClass, collection::Collection, id::Id, type_state::DataType};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub trait BlobOperations: Sync + Send {
|
||||
fn blob_get(
|
||||
&self,
|
||||
request: GetRequest<Blob>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<GetResponse<Blob>>> + Send;
|
||||
|
||||
fn blob_lookup(
|
||||
&self,
|
||||
request: BlobLookupRequest,
|
||||
) -> impl Future<Output = trc::Result<BlobLookupResponse>> + Send;
|
||||
}
|
||||
|
||||
impl BlobOperations for Server {
|
||||
async fn blob_get(
|
||||
&self,
|
||||
mut request: GetRequest<Blob>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<GetResponse<Blob>> {
|
||||
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let ids = ids.unwrap_or_default();
|
||||
let properties = request.unwrap_properties(&[
|
||||
BlobProperty::Id,
|
||||
BlobProperty::Data(DataProperty::Default),
|
||||
BlobProperty::Size,
|
||||
]);
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
state: None,
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: not_found_ids,
|
||||
};
|
||||
|
||||
let range_from = request.arguments.offset.unwrap_or(0);
|
||||
let range_to = request
|
||||
.arguments
|
||||
.length
|
||||
.map(|length| range_from.saturating_add(length))
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
for blob_id in ids {
|
||||
if let Some(bytes) = self.blob_download(&blob_id, access_token).await? {
|
||||
let mut blob = Map::with_capacity(properties.len());
|
||||
let bytes_range = if range_from == 0 && range_to == usize::MAX {
|
||||
&bytes[..]
|
||||
} else {
|
||||
let range_to = if range_to != usize::MAX && range_to > bytes.len() {
|
||||
blob.insert_unchecked(BlobProperty::IsTruncated, true);
|
||||
bytes.len()
|
||||
} else {
|
||||
range_to
|
||||
};
|
||||
bytes.get(range_from..range_to).unwrap_or_default()
|
||||
};
|
||||
|
||||
for property in &properties {
|
||||
let mut property = property.clone();
|
||||
let value: Value<'static, BlobProperty, BlobValue> = match &property {
|
||||
BlobProperty::Id => Value::Element(BlobValue::BlobId(blob_id.clone())),
|
||||
BlobProperty::Size => bytes.len().into(),
|
||||
BlobProperty::Digest(digest) => match digest {
|
||||
DigestProperty::Sha => {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(bytes_range);
|
||||
String::from_utf8(
|
||||
Base64Encoder::new()
|
||||
.encode(&hasher.finalize()[..])
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
DigestProperty::Sha256 => {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(bytes_range);
|
||||
String::from_utf8(
|
||||
Base64Encoder::new()
|
||||
.encode(&hasher.finalize()[..])
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
DigestProperty::Sha512 => {
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(bytes_range);
|
||||
String::from_utf8(
|
||||
Base64Encoder::new()
|
||||
.encode(&hasher.finalize()[..])
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
.into(),
|
||||
BlobProperty::Data(data) => match data {
|
||||
DataProperty::AsText => match std::str::from_utf8(bytes_range) {
|
||||
Ok(text) => text.to_string().into(),
|
||||
Err(_) => {
|
||||
blob.insert_unchecked(BlobProperty::IsEncodingProblem, true);
|
||||
Value::Null
|
||||
}
|
||||
},
|
||||
DataProperty::AsBase64 => String::from_utf8(
|
||||
Base64Encoder::new().encode(bytes_range).unwrap_or_default(),
|
||||
)
|
||||
.unwrap()
|
||||
.into(),
|
||||
DataProperty::Default => match std::str::from_utf8(bytes_range) {
|
||||
Ok(text) => {
|
||||
property = BlobProperty::Data(DataProperty::AsText);
|
||||
text.to_string().into()
|
||||
}
|
||||
Err(_) => {
|
||||
property = BlobProperty::Data(DataProperty::AsBase64);
|
||||
blob.insert_unchecked(BlobProperty::IsEncodingProblem, true);
|
||||
String::from_utf8(
|
||||
Base64Encoder::new()
|
||||
.encode(bytes_range)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
},
|
||||
},
|
||||
_ => Value::Null,
|
||||
};
|
||||
blob.insert_unchecked(property, value);
|
||||
}
|
||||
|
||||
// Add result to response
|
||||
response.list.push(blob.into());
|
||||
} else {
|
||||
response.push_not_found(blob_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn blob_lookup(&self, request: BlobLookupRequest) -> trc::Result<BlobLookupResponse> {
|
||||
let mut include_email = false;
|
||||
let mut include_mailbox = false;
|
||||
let mut include_thread = false;
|
||||
|
||||
let type_names = request
|
||||
.type_names
|
||||
.into_iter()
|
||||
.map(|tn| match tn {
|
||||
MaybeInvalid::Value(value) => {
|
||||
match &value {
|
||||
DataType::Email => {
|
||||
include_email = true;
|
||||
}
|
||||
DataType::Mailbox => {
|
||||
include_mailbox = true;
|
||||
}
|
||||
DataType::Thread => {
|
||||
include_thread = true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
MaybeInvalid::Invalid(_) => Err(trc::JmapEvent::UnknownDataType.into_err()),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let req_account_id = request.account_id.document_id();
|
||||
let mut response = BlobLookupResponse {
|
||||
account_id: request.account_id,
|
||||
list: Vec::with_capacity(request.ids.len()),
|
||||
not_found: vec![],
|
||||
};
|
||||
|
||||
for id in request.ids.into_valid() {
|
||||
let mut matched_ids = VecMap::new();
|
||||
|
||||
match &id.class {
|
||||
BlobClass::Linked {
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
} if *account_id == req_account_id => {
|
||||
let collection = Collection::from(*collection);
|
||||
if collection == Collection::Email {
|
||||
if let Some(data_) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
req_account_id,
|
||||
Collection::Email,
|
||||
*document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
let data = data_
|
||||
.unarchive::<MessageData>()
|
||||
.caused_by(trc::location!())?;
|
||||
if include_email {
|
||||
matched_ids.append(
|
||||
DataType::Email,
|
||||
vec![Id::from_parts(u32::from(data.thread_id), *document_id)],
|
||||
);
|
||||
}
|
||||
if include_thread {
|
||||
matched_ids.append(
|
||||
DataType::Thread,
|
||||
vec![Id::from(u32::from(data.thread_id))],
|
||||
);
|
||||
}
|
||||
if include_mailbox {
|
||||
matched_ids.append(
|
||||
DataType::Mailbox,
|
||||
data.mailboxes
|
||||
.iter()
|
||||
.map(|m| {
|
||||
debug_assert!(m.uid != 0);
|
||||
Id::from(u32::from(m.mailbox_id))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match DataType::try_from(collection) {
|
||||
Ok(data_type) if type_names.contains(&data_type) => {
|
||||
matched_ids.append(data_type, vec![Id::from(*document_id)]);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
BlobClass::Reserved { account_id, .. } if *account_id == req_account_id => {}
|
||||
_ => {
|
||||
response.not_found.push(id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
response.list.push(BlobInfo { id, matched_ids });
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
pub mod copy;
|
||||
pub mod download;
|
||||
pub mod get;
|
||||
pub mod upload;
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct UploadResponse {
|
||||
#[serde(rename(serialize = "accountId"))]
|
||||
account_id: Id,
|
||||
#[serde(rename(serialize = "blobId"))]
|
||||
blob_id: BlobId,
|
||||
#[serde(rename(serialize = "type"))]
|
||||
c_type: String,
|
||||
size: usize,
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{UploadResponse, download::BlobDownload};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
method::upload::{
|
||||
BlobUploadRequest, BlobUploadResponse, BlobUploadResponseObject, DataSourceObject,
|
||||
},
|
||||
request::reference::MaybeIdReference,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::future::Future;
|
||||
use trc::AddContext;
|
||||
use types::id::Id;
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
pub static DISABLE_UPLOAD_QUOTA: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(true);
|
||||
|
||||
pub trait BlobUpload: Sync + Send {
|
||||
fn blob_upload_many(
|
||||
&self,
|
||||
request: BlobUploadRequest,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<BlobUploadResponse>> + Send;
|
||||
|
||||
fn blob_upload(
|
||||
&self,
|
||||
account_id: Id,
|
||||
content_type: &str,
|
||||
data: &[u8],
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<UploadResponse>> + Send;
|
||||
}
|
||||
|
||||
impl BlobUpload for Server {
|
||||
async fn blob_upload_many(
|
||||
&self,
|
||||
request: BlobUploadRequest,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<BlobUploadResponse> {
|
||||
let mut response = BlobUploadResponse {
|
||||
account_id: request.account_id,
|
||||
created: Default::default(),
|
||||
not_created: Default::default(),
|
||||
};
|
||||
let account_id = request.account_id.document_id();
|
||||
|
||||
if request.create.len() > self.core.jmap.set_max_objects {
|
||||
return Err(trc::JmapEvent::RequestTooLarge.into_err());
|
||||
}
|
||||
|
||||
'outer: for (create_id, upload_object) in request.create {
|
||||
let mut data = Vec::new();
|
||||
|
||||
for data_source in upload_object.data {
|
||||
let bytes = match data_source {
|
||||
DataSourceObject::Id { id, length, offset } => {
|
||||
let id = match id {
|
||||
MaybeIdReference::Id(id) => id,
|
||||
MaybeIdReference::Reference(reference) => {
|
||||
if let Some(obj) = response.created.get(&reference) {
|
||||
obj.id.clone()
|
||||
} else {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::not_found().with_description(format!(
|
||||
"Id reference {reference:?} not found."
|
||||
)),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
MaybeIdReference::Invalid(id) => {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::invalid_properties()
|
||||
.with_description(format!("Invalid blobId {id}.")),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
};
|
||||
|
||||
if !self.has_access_blob(&id, access_token).await? {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::forbidden().with_description(format!(
|
||||
"You do not have access to blobId {id}."
|
||||
)),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
|
||||
let offset = offset.unwrap_or(0);
|
||||
let length = length
|
||||
.map(|length| length.saturating_add(offset))
|
||||
.unwrap_or(usize::MAX);
|
||||
let bytes = if let Some(section) = &id.section {
|
||||
self.get_blob_section(&id.hash, section)
|
||||
.await?
|
||||
.map(|bytes| {
|
||||
if offset == 0 && length == usize::MAX {
|
||||
bytes
|
||||
} else {
|
||||
bytes
|
||||
.get(offset..std::cmp::min(length, bytes.len()))
|
||||
.unwrap_or_default()
|
||||
.to_vec()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.blob_store()
|
||||
.get_blob(id.hash.as_slice(), offset..length)
|
||||
.await?
|
||||
};
|
||||
if let Some(bytes) = bytes {
|
||||
bytes
|
||||
} else {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::blob_not_found()
|
||||
.with_description(format!("BlobId {id} not found.")),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
DataSourceObject::Value(bytes) => bytes,
|
||||
DataSourceObject::Null => {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::invalid_properties()
|
||||
.with_description("Invalid DataSourceObject."),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
};
|
||||
|
||||
if bytes.len() + data.len() < self.core.jmap.upload_max_size {
|
||||
data.extend(bytes);
|
||||
} else {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::too_large().with_description(format!(
|
||||
"Upload size exceeds maximum of {} bytes.",
|
||||
self.core.jmap.upload_max_size
|
||||
)),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
if data.is_empty() {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::invalid_properties()
|
||||
.with_description("Must specify at least one valid DataSourceObject."),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
|
||||
// Enforce quota
|
||||
if !access_token.has_permission(Permission::UnlimitedUploads)
|
||||
&& !self
|
||||
.blob_has_quota(account_id, data.len())
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.allowed
|
||||
{
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::over_quota().with_description(format!(
|
||||
"You have exceeded the blob upload quota of {} files or {} bytes.",
|
||||
self.core.jmap.upload_tmp_quota_amount,
|
||||
self.core.jmap.upload_tmp_quota_size
|
||||
)),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
|
||||
// Write blob
|
||||
response.created.insert(
|
||||
create_id,
|
||||
BlobUploadResponseObject {
|
||||
id: self.put_jmap_blob(account_id, &data).await?,
|
||||
type_: upload_object.type_,
|
||||
size: data.len(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn blob_upload(
|
||||
&self,
|
||||
account_id: Id,
|
||||
content_type: &str,
|
||||
data: &[u8],
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<UploadResponse> {
|
||||
// Limit concurrent uploads
|
||||
let _in_flight = self
|
||||
.is_upload_allowed(access_token)
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
{
|
||||
// Used for concurrent upload tests
|
||||
if data == b"sleep" {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce quota
|
||||
if !access_token.has_permission(Permission::UnlimitedUploads) {
|
||||
let status = self
|
||||
.blob_has_quota(account_id.document_id(), data.len())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if !status.allowed {
|
||||
let err = Err(trc::LimitEvent::BlobQuota
|
||||
.into_err()
|
||||
.ctx(trc::Key::Size, self.core.jmap.upload_tmp_quota_size)
|
||||
.ctx(trc::Key::Total, self.core.jmap.upload_tmp_quota_amount)
|
||||
.ctx(trc::Key::Expires, status.expires_in));
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
if !DISABLE_UPLOAD_QUOTA.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return err;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(UploadResponse {
|
||||
account_id,
|
||||
blob_id: self
|
||||
.put_jmap_blob(account_id.document_id(), data)
|
||||
.await
|
||||
.caused_by(trc::location!())?,
|
||||
c_type: content_type.to_string(),
|
||||
size: data.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user