/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * 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> + Send; fn blob_upload( &self, account_id: Id, content_type: &str, data: &[u8], access_token: &AccessToken, ) -> impl Future> + Send; } impl BlobUpload for Server { async fn blob_upload_many( &self, request: BlobUploadRequest, access_token: &AccessToken, ) -> trc::Result { 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 { // 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(), }) } }