1#![forbid(unsafe_code)]
3
4use crate::{ObjectKey, ObjectStore, ObjectStoreError, PresignedObjectRequest, StoredObject};
5use async_trait::async_trait;
6use chrono::{DateTime, TimeDelta, Utc};
7use futures::{Stream, stream};
8use serde::{Deserialize, Serialize};
9use std::{
10 collections::{BTreeMap, BTreeSet},
11 fmt,
12 pin::Pin,
13 sync::Arc,
14};
15use uuid::Uuid;
16
17pub const MIN_MULTIPART_PART_SIZE_BYTES: u64 = 5 * 1024 * 1024;
18pub const MAX_MULTIPART_PART_SIZE_BYTES: u64 = 5 * 1024 * 1024 * 1024;
19pub const MAX_MULTIPART_PARTS: u32 = 10_000;
20pub const MAX_MULTIPART_OBJECT_SIZE_BYTES: u64 =
21 MAX_MULTIPART_PART_SIZE_BYTES * MAX_MULTIPART_PARTS as u64;
22const MAX_CAPABILITY_EXPIRY_SECONDS: i64 = 24 * 60 * 60;
23const DEFAULT_PART_EXPIRY_SECONDS: i64 = 15 * 60;
24const RESERVED_ATTRIBUTE_PREFIX: &str = "minco.";
25const UPLOAD_ID_ATTRIBUTE: &str = "minco.upload_id";
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(tag = "kind", rename_all = "snake_case")]
31pub enum ObjectByteRange {
32 Bounded { start: u64, end_exclusive: u64 },
33 From { start: u64 },
34 Suffix { length: u64 },
35}
36
37impl ObjectByteRange {
38 pub const fn bounded(start: u64, end_exclusive: u64) -> Result<Self, ObjectTransferError> {
39 if start >= end_exclusive {
40 Err(ObjectTransferError::InvalidRange)
41 } else {
42 Ok(Self::Bounded {
43 start,
44 end_exclusive,
45 })
46 }
47 }
48
49 pub const fn from(start: u64) -> Self {
50 Self::From { start }
51 }
52
53 pub const fn suffix(length: u64) -> Result<Self, ObjectTransferError> {
54 if length == 0 {
55 Err(ObjectTransferError::InvalidRange)
56 } else {
57 Ok(Self::Suffix { length })
58 }
59 }
60
61 #[must_use]
62 pub fn to_http_value(self) -> String {
63 match self {
64 Self::Bounded {
65 start,
66 end_exclusive,
67 } => format!("bytes={start}-{}", end_exclusive - 1),
68 Self::From { start } => format!("bytes={start}-"),
69 Self::Suffix { length } => format!("bytes=-{length}"),
70 }
71 }
72
73 pub const fn validate(self) -> Result<(), ObjectTransferError> {
74 match self {
75 Self::Bounded {
76 start,
77 end_exclusive,
78 } if start < end_exclusive => Ok(()),
79 Self::From { .. } => Ok(()),
80 Self::Suffix { length } if length > 0 => Ok(()),
81 _ => Err(ObjectTransferError::InvalidRange),
82 }
83 }
84
85 fn resolve(self, size_bytes: u64) -> Result<(u64, u64), ObjectTransferError> {
86 if size_bytes == 0 {
87 return Err(ObjectTransferError::RangeNotSatisfiable);
88 }
89 match self {
90 Self::Bounded {
91 start,
92 end_exclusive,
93 } if start < end_exclusive && start < size_bytes => {
94 Ok((start, end_exclusive.min(size_bytes)))
95 }
96 Self::From { start } if start < size_bytes => Ok((start, size_bytes)),
97 Self::Suffix { length } if length > 0 => {
98 Ok((size_bytes.saturating_sub(length), size_bytes))
99 }
100 _ => Err(ObjectTransferError::RangeNotSatisfiable),
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct ObjectReadHead {
108 pub key: ObjectKey,
109 pub content_type: String,
110 pub size_bytes: u64,
111 pub entity_tag: String,
112 pub version_id: Option<String>,
113 pub last_modified: DateTime<Utc>,
114 #[serde(default)]
115 pub attributes: BTreeMap<String, String>,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct ObjectReadRequest {
120 pub key: ObjectKey,
121 pub range: Option<ObjectByteRange>,
122 pub expected_entity_tag: Option<String>,
123 pub version_id: Option<String>,
124}
125
126pub type ObjectByteStream =
127 Pin<Box<dyn Stream<Item = Result<Vec<u8>, ObjectStoreError>> + Send + 'static>>;
128
129pub struct ObjectReadResponse {
130 pub head: ObjectReadHead,
131 pub content_range: Option<String>,
132 pub stream: ObjectByteStream,
133}
134
135impl fmt::Debug for ObjectReadResponse {
136 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137 formatter
138 .debug_struct("ObjectReadResponse")
139 .field("head", &self.head)
140 .field("content_range", &self.content_range)
141 .field("stream", &"[STREAM]")
142 .finish()
143 }
144}
145
146#[async_trait]
149pub trait ObjectStreamReader: Send + Sync + fmt::Debug {
150 async fn head(&self, key: &ObjectKey) -> Result<Option<ObjectReadHead>, ObjectStoreError>;
151 async fn read(
152 &self,
153 request: ObjectReadRequest,
154 ) -> Result<Option<ObjectReadResponse>, ObjectStoreError>;
155}
156
157#[async_trait]
158impl ObjectStreamReader for crate::MemoryObjectStore {
159 async fn head(&self, key: &ObjectKey) -> Result<Option<ObjectReadHead>, ObjectStoreError> {
160 let object = ObjectStore::get(self, key).await?;
161 Ok(object.map(|object| read_head(&object)))
162 }
163
164 async fn read(
165 &self,
166 request: ObjectReadRequest,
167 ) -> Result<Option<ObjectReadResponse>, ObjectStoreError> {
168 let Some(object) = ObjectStore::get(self, &request.key).await? else {
169 return Ok(None);
170 };
171 let head = read_head(&object);
172 validate_read_preconditions(&head, &request).map_err(|error| transfer_to_store(&error))?;
173 let (bytes, content_range) = match request.range {
174 Some(range) => {
175 let (start, end) = range
176 .resolve(head.size_bytes)
177 .map_err(|error| transfer_to_store(&error))?;
178 let start = usize::try_from(start).map_err(|_| ObjectStoreError::ObjectTooLarge)?;
179 let end = usize::try_from(end).map_err(|_| ObjectStoreError::ObjectTooLarge)?;
180 (
181 object.bytes[start..end].to_vec(),
182 Some(format!(
183 "bytes {start}-{}/{size}",
184 end - 1,
185 size = head.size_bytes
186 )),
187 )
188 }
189 None => (object.bytes, None),
190 };
191 Ok(Some(ObjectReadResponse {
192 head,
193 content_range,
194 stream: Box::pin(stream::once(async move { Ok(bytes) })),
195 }))
196 }
197}
198
199fn read_head(object: &StoredObject) -> ObjectReadHead {
200 ObjectReadHead {
201 key: object.key.clone(),
202 content_type: object.metadata.content_type.clone(),
203 size_bytes: object.metadata.size_bytes,
204 entity_tag: format!("\"{}\"", object.metadata.sha256),
205 version_id: None,
206 last_modified: object.metadata.created_at,
207 attributes: object.metadata.attributes.clone(),
208 }
209}
210
211#[derive(Clone)]
212pub struct ObjectReadService(Arc<dyn ObjectStreamReader>);
213
214impl fmt::Debug for ObjectReadService {
215 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
216 formatter.debug_tuple("ObjectReadService").finish()
217 }
218}
219
220impl ObjectReadService {
221 pub fn new(reader: Arc<dyn ObjectStreamReader>) -> Self {
222 Self(reader)
223 }
224
225 pub async fn head(&self, key: &ObjectKey) -> Result<Option<ObjectReadHead>, ObjectStoreError> {
226 self.0.head(key).await
227 }
228
229 pub async fn read(
230 &self,
231 request: ObjectReadRequest,
232 ) -> Result<Option<ObjectReadResponse>, ObjectStoreError> {
233 self.0.read(request).await
234 }
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(tag = "kind", rename_all = "snake_case")]
239pub enum DownloadCachePolicy {
240 NoStore,
241 Private {
242 max_age_seconds: u32,
243 immutable: bool,
244 },
245}
246
247impl DownloadCachePolicy {
248 #[must_use]
249 pub fn to_header_value(self) -> String {
250 match self {
251 Self::NoStore => "private, no-store".into(),
252 Self::Private {
253 max_age_seconds,
254 immutable: true,
255 } => format!("private, max-age={max_age_seconds}, immutable"),
256 Self::Private {
257 max_age_seconds,
258 immutable: false,
259 } => format!("private, max-age={max_age_seconds}"),
260 }
261 }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct ObjectDownloadPolicy {
266 expires_in: TimeDelta,
267 cache: DownloadCachePolicy,
268}
269
270impl ObjectDownloadPolicy {
271 pub fn new(
272 expires_in: TimeDelta,
273 cache: DownloadCachePolicy,
274 ) -> Result<Self, ObjectTransferError> {
275 validate_expiry(expires_in)?;
276 Ok(Self { expires_in, cache })
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281pub struct IssueObjectDownload {
282 pub key: ObjectKey,
283 pub range: Option<ObjectByteRange>,
284 pub expected_entity_tag: Option<String>,
285 pub version_id: Option<String>,
286 pub download_file_name: Option<String>,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq)]
290pub struct SignObjectDownload {
291 pub key: ObjectKey,
292 pub range: Option<ObjectByteRange>,
293 pub if_match: String,
294 pub version_id: Option<String>,
295 pub download_file_name: Option<String>,
296 pub cache_control: String,
297 pub expires_in: TimeDelta,
298}
299
300#[async_trait]
301pub trait ObjectDownloadSigner: Send + Sync + fmt::Debug {
302 async fn sign_download(
303 &self,
304 request: SignObjectDownload,
305 ) -> Result<PresignedObjectRequest, ObjectTransferError>;
306}
307
308#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct ObjectDownloadGrant {
312 pub key: ObjectKey,
313 pub request: PresignedObjectRequest,
314 pub content_type: String,
315 pub size_bytes: u64,
316 pub entity_tag: String,
317 pub version_id: Option<String>,
318 pub last_modified: DateTime<Utc>,
319 pub range: Option<ObjectByteRange>,
320 pub cache_control: String,
321}
322
323impl fmt::Debug for ObjectDownloadGrant {
324 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
325 formatter
326 .debug_struct("ObjectDownloadGrant")
327 .field("key", &self.key)
328 .field("request", &self.request)
329 .field("content_type", &self.content_type)
330 .field("size_bytes", &self.size_bytes)
331 .field("entity_tag", &self.entity_tag)
332 .field("version_id", &self.version_id)
333 .field("last_modified", &self.last_modified)
334 .field("range", &self.range)
335 .field("cache_control", &self.cache_control)
336 .finish()
337 }
338}
339
340#[derive(Clone)]
341pub struct ObjectDownloadService {
342 signer: Arc<dyn ObjectDownloadSigner>,
343 reads: ObjectReadService,
344 policy: ObjectDownloadPolicy,
345}
346
347impl fmt::Debug for ObjectDownloadService {
348 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
349 formatter
350 .debug_struct("ObjectDownloadService")
351 .field("policy", &self.policy)
352 .finish_non_exhaustive()
353 }
354}
355
356impl ObjectDownloadService {
357 pub fn new(
358 signer: Arc<dyn ObjectDownloadSigner>,
359 reads: ObjectReadService,
360 policy: ObjectDownloadPolicy,
361 ) -> Self {
362 Self {
363 signer,
364 reads,
365 policy,
366 }
367 }
368
369 pub async fn issue(
370 &self,
371 request: IssueObjectDownload,
372 ) -> Result<ObjectDownloadGrant, ObjectTransferError> {
373 let head = self
374 .reads
375 .head(&request.key)
376 .await?
377 .ok_or(ObjectTransferError::MissingObject)?;
378 validate_read_preconditions(
379 &head,
380 &ObjectReadRequest {
381 key: request.key.clone(),
382 range: request.range,
383 expected_entity_tag: request.expected_entity_tag,
384 version_id: request.version_id,
385 },
386 )?;
387 if let Some(range) = request.range {
388 range.resolve(head.size_bytes)?;
389 }
390 let download_file_name = request
391 .download_file_name
392 .unwrap_or_else(|| "download".into());
393 validate_download_name(&download_file_name)?;
394 let cache_control = self.policy.cache.to_header_value();
395 let signed = self
396 .signer
397 .sign_download(SignObjectDownload {
398 key: request.key.clone(),
399 range: request.range,
400 if_match: head.entity_tag.clone(),
401 version_id: head.version_id.clone(),
402 download_file_name: Some(download_file_name),
403 cache_control: cache_control.clone(),
404 expires_in: self.policy.expires_in,
405 })
406 .await?;
407 Ok(ObjectDownloadGrant {
408 key: request.key,
409 request: signed,
410 content_type: head.content_type,
411 size_bytes: head.size_bytes,
412 entity_tag: head.entity_tag,
413 version_id: head.version_id,
414 last_modified: head.last_modified,
415 range: request.range,
416 cache_control,
417 })
418 }
419}
420
421fn validate_read_preconditions(
422 head: &ObjectReadHead,
423 request: &ObjectReadRequest,
424) -> Result<(), ObjectTransferError> {
425 if request
426 .expected_entity_tag
427 .as_deref()
428 .is_some_and(|expected| expected != head.entity_tag)
429 || request
430 .version_id
431 .as_deref()
432 .is_some_and(|expected| Some(expected) != head.version_id.as_deref())
433 {
434 return Err(ObjectTransferError::PreconditionFailed);
435 }
436 Ok(())
437}
438
439#[derive(Clone, PartialEq, Eq, Serialize)]
442#[serde(transparent)]
443pub struct ProviderMultipartUploadId(String);
444
445impl ProviderMultipartUploadId {
446 pub fn parse(value: impl Into<String>) -> Result<Self, ObjectTransferError> {
447 let value = value.into();
448 if value.is_empty() || value.len() > 2_048 || value.chars().any(char::is_control) {
449 Err(ObjectTransferError::InvalidProviderUploadId)
450 } else {
451 Ok(Self(value))
452 }
453 }
454
455 pub fn expose_secret(&self) -> &str {
457 &self.0
458 }
459}
460
461impl<'de> Deserialize<'de> for ProviderMultipartUploadId {
462 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
463 where
464 D: serde::Deserializer<'de>,
465 {
466 let value = String::deserialize(deserializer)?;
467 Self::parse(value)
468 .map_err(|_| serde::de::Error::custom("invalid provider multipart upload ID"))
469 }
470}
471
472impl fmt::Debug for ProviderMultipartUploadId {
473 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
474 formatter.write_str("ProviderMultipartUploadId([REDACTED])")
475 }
476}
477
478#[derive(Debug, Clone, PartialEq, Eq)]
479pub struct MultipartUploadPolicy {
480 key_prefix: ObjectKey,
481 maximum_size_bytes: u64,
482 part_size_bytes: u64,
483 allowed_content_types: BTreeSet<String>,
484 part_expires_in: TimeDelta,
485}
486
487impl MultipartUploadPolicy {
488 pub fn new<I, S>(
489 key_prefix: ObjectKey,
490 maximum_size_bytes: u64,
491 part_size_bytes: u64,
492 allowed_content_types: I,
493 ) -> Result<Self, ObjectTransferError>
494 where
495 I: IntoIterator<Item = S>,
496 S: AsRef<str>,
497 {
498 if maximum_size_bytes == 0 || maximum_size_bytes > MAX_MULTIPART_OBJECT_SIZE_BYTES {
499 return Err(ObjectTransferError::InvalidMaximumSize);
500 }
501 if !(MIN_MULTIPART_PART_SIZE_BYTES..=MAX_MULTIPART_PART_SIZE_BYTES)
502 .contains(&part_size_bytes)
503 {
504 return Err(ObjectTransferError::InvalidPartSize);
505 }
506 let allowed_content_types = allowed_content_types
507 .into_iter()
508 .map(|value| normalize_content_type(value.as_ref()))
509 .collect::<Result<BTreeSet<_>, _>>()?;
510 if allowed_content_types.is_empty() {
511 return Err(ObjectTransferError::EmptyContentTypeAllowlist);
512 }
513 let policy = Self {
514 key_prefix,
515 maximum_size_bytes,
516 part_size_bytes,
517 allowed_content_types,
518 part_expires_in: TimeDelta::seconds(DEFAULT_PART_EXPIRY_SECONDS),
519 };
520 policy.plan(maximum_size_bytes)?;
521 Ok(policy)
522 }
523
524 pub fn with_part_expiry(mut self, expires_in: TimeDelta) -> Result<Self, ObjectTransferError> {
525 validate_expiry(expires_in)?;
526 self.part_expires_in = expires_in;
527 Ok(self)
528 }
529
530 pub fn plan(&self, size_bytes: u64) -> Result<MultipartUploadPlan, ObjectTransferError> {
531 if size_bytes == 0 {
532 return Err(ObjectTransferError::EmptyObject);
533 }
534 if size_bytes > self.maximum_size_bytes {
535 return Err(ObjectTransferError::ObjectTooLarge {
536 actual: size_bytes,
537 maximum: self.maximum_size_bytes,
538 });
539 }
540 let count = size_bytes.div_ceil(self.part_size_bytes);
541 let part_count = u32::try_from(count).map_err(|_| ObjectTransferError::TooManyParts)?;
542 if part_count == 0 || part_count > MAX_MULTIPART_PARTS {
543 return Err(ObjectTransferError::TooManyParts);
544 }
545 Ok(MultipartUploadPlan {
546 size_bytes,
547 part_size_bytes: self.part_size_bytes,
548 part_count,
549 })
550 }
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
554pub struct MultipartUploadPlan {
555 pub size_bytes: u64,
556 pub part_size_bytes: u64,
557 pub part_count: u32,
558}
559
560impl MultipartUploadPlan {
561 pub fn expected_part_size(&self, part_number: u32) -> Result<u64, ObjectTransferError> {
562 if part_number == 0 || part_number > self.part_count {
563 return Err(ObjectTransferError::InvalidPartNumber);
564 }
565 if part_number < self.part_count {
566 Ok(self.part_size_bytes)
567 } else {
568 Ok(self.size_bytes - self.part_size_bytes * u64::from(self.part_count - 1))
569 }
570 }
571}
572
573#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
574pub struct IssueMultipartObjectUpload {
575 pub content_type: String,
576 pub size_bytes: u64,
577 #[serde(default)]
578 pub attributes: BTreeMap<String, String>,
579}
580
581#[derive(Debug, Clone, PartialEq, Eq)]
582pub struct SignMultipartObject {
583 pub key: ObjectKey,
584 pub content_type: String,
585 pub size_bytes: u64,
586 pub attributes: BTreeMap<String, String>,
587}
588
589#[derive(Debug, Clone, PartialEq, Eq)]
590pub struct SignMultipartPart {
591 pub key: ObjectKey,
592 pub upload_id: ProviderMultipartUploadId,
593 pub part_number: u32,
594 pub size_bytes: u64,
595 pub sha256: String,
597 pub expires_in: TimeDelta,
598}
599
600#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601#[serde(deny_unknown_fields)]
602pub struct MultipartPartReceipt {
603 pub part_number: u32,
604 pub entity_tag: String,
605 pub sha256: String,
607}
608
609#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
610pub struct TrustedMultipartPart {
611 pub part_number: u32,
612 pub size_bytes: u64,
613 pub entity_tag: String,
614 pub sha256: String,
615}
616
617#[derive(Debug, Clone, PartialEq, Eq)]
618pub struct CompleteMultipartObject {
619 pub key: ObjectKey,
620 pub upload_id: ProviderMultipartUploadId,
621 pub content_type: String,
622 pub size_bytes: u64,
623 pub attributes: BTreeMap<String, String>,
624 pub parts: Vec<TrustedMultipartPart>,
625}
626
627#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
628pub struct CompletedMultipartObject {
629 pub key: ObjectKey,
630 pub content_type: String,
631 pub size_bytes: u64,
632 pub entity_tag: Option<String>,
633 pub version_id: Option<String>,
634 #[serde(default)]
635 pub attributes: BTreeMap<String, String>,
636}
637
638#[async_trait]
639pub trait MultipartObjectSigner: Send + Sync + fmt::Debug {
640 async fn initiate_multipart(
641 &self,
642 request: SignMultipartObject,
643 ) -> Result<ProviderMultipartUploadId, ObjectTransferError>;
644 async fn sign_multipart_part(
645 &self,
646 request: SignMultipartPart,
647 ) -> Result<PresignedObjectRequest, ObjectTransferError>;
648 async fn complete_multipart(
649 &self,
650 request: CompleteMultipartObject,
651 ) -> Result<CompletedMultipartObject, ObjectTransferError>;
652 async fn abort_multipart(
653 &self,
654 key: &ObjectKey,
655 upload_id: &ProviderMultipartUploadId,
656 ) -> Result<(), ObjectTransferError>;
657}
658
659#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
660pub struct MultipartUploadGrant {
661 pub upload_id: Uuid,
662 pub key: ObjectKey,
663 pub size_bytes: u64,
664 pub part_size_bytes: u64,
665 pub part_count: u32,
666}
667
668impl MultipartUploadGrant {
669 pub fn expected_part_size(&self, part_number: u32) -> Result<u64, ObjectTransferError> {
670 MultipartUploadPlan {
671 size_bytes: self.size_bytes,
672 part_size_bytes: self.part_size_bytes,
673 part_count: self.part_count,
674 }
675 .expected_part_size(part_number)
676 }
677}
678
679#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
680pub struct PendingMultipartUpload {
681 pub upload_id: Uuid,
682 pub key: ObjectKey,
683 pub provider_upload_id: ProviderMultipartUploadId,
684 pub expected_content_type: String,
685 pub expected_size_bytes: u64,
686 pub expected_attributes: BTreeMap<String, String>,
687 pub part_size_bytes: u64,
688 pub part_count: u32,
689}
690
691impl fmt::Debug for PendingMultipartUpload {
692 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
693 formatter
694 .debug_struct("PendingMultipartUpload")
695 .field("upload_id", &self.upload_id)
696 .field("key", &self.key)
697 .field("provider_upload_id", &"[REDACTED]")
698 .field("expected_content_type", &self.expected_content_type)
699 .field("expected_size_bytes", &self.expected_size_bytes)
700 .field("expected_attribute_names", &self.expected_attributes.keys())
701 .field("part_size_bytes", &self.part_size_bytes)
702 .field("part_count", &self.part_count)
703 .finish()
704 }
705}
706
707#[derive(Debug, Clone, PartialEq, Eq)]
708pub struct IssuedMultipartUpload {
709 pub grant: MultipartUploadGrant,
710 pub pending: PendingMultipartUpload,
711}
712
713#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
714pub struct MultipartPartGrant {
715 pub upload_id: Uuid,
716 pub part_number: u32,
717 pub size_bytes: u64,
718 pub request: PresignedObjectRequest,
719}
720
721impl fmt::Debug for MultipartPartGrant {
722 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
723 formatter
724 .debug_struct("MultipartPartGrant")
725 .field("upload_id", &self.upload_id)
726 .field("part_number", &self.part_number)
727 .field("size_bytes", &self.size_bytes)
728 .field("request", &self.request)
729 .finish()
730 }
731}
732
733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
734pub struct ExpectedMultipartPart {
735 pub upload_id: Uuid,
736 pub part_number: u32,
737 pub size_bytes: u64,
738 pub sha256: String,
739}
740
741#[derive(Debug, Clone, PartialEq, Eq)]
742pub struct IssuedMultipartPart {
743 pub grant: MultipartPartGrant,
744 pub expected: ExpectedMultipartPart,
745}
746
747#[derive(Clone)]
748pub struct MultipartObjectService {
749 signer: Arc<dyn MultipartObjectSigner>,
750 policy: MultipartUploadPolicy,
751}
752
753impl fmt::Debug for MultipartObjectService {
754 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
755 formatter
756 .debug_struct("MultipartObjectService")
757 .field("policy", &self.policy)
758 .finish_non_exhaustive()
759 }
760}
761
762impl MultipartObjectService {
763 pub fn new(signer: Arc<dyn MultipartObjectSigner>, policy: MultipartUploadPolicy) -> Self {
764 Self { signer, policy }
765 }
766
767 pub async fn issue(
768 &self,
769 request: IssueMultipartObjectUpload,
770 ) -> Result<IssuedMultipartUpload, ObjectTransferError> {
771 let content_type = normalize_content_type(&request.content_type)?;
772 if !self.policy.allowed_content_types.contains(&content_type) {
773 return Err(ObjectTransferError::UnsupportedContentType(content_type));
774 }
775 let plan = self.policy.plan(request.size_bytes)?;
776 let upload_id = Uuid::now_v7();
777 let key = ObjectKey::parse(format!("{}/{upload_id}", self.policy.key_prefix.as_str()))?;
778 let mut attributes = validate_attributes(request.attributes)?;
779 attributes.insert(UPLOAD_ID_ATTRIBUTE.into(), upload_id.to_string());
780 let provider_upload_id = self
781 .signer
782 .initiate_multipart(SignMultipartObject {
783 key: key.clone(),
784 content_type: content_type.clone(),
785 size_bytes: request.size_bytes,
786 attributes: attributes.clone(),
787 })
788 .await?;
789 Ok(IssuedMultipartUpload {
790 grant: MultipartUploadGrant {
791 upload_id,
792 key: key.clone(),
793 size_bytes: plan.size_bytes,
794 part_size_bytes: plan.part_size_bytes,
795 part_count: plan.part_count,
796 },
797 pending: PendingMultipartUpload {
798 upload_id,
799 key,
800 provider_upload_id,
801 expected_content_type: content_type,
802 expected_size_bytes: plan.size_bytes,
803 expected_attributes: attributes,
804 part_size_bytes: plan.part_size_bytes,
805 part_count: plan.part_count,
806 },
807 })
808 }
809
810 pub async fn issue_part(
811 &self,
812 pending: &PendingMultipartUpload,
813 part_number: u32,
814 sha256: String,
815 ) -> Result<IssuedMultipartPart, ObjectTransferError> {
816 self.validate_pending(pending)?;
817 let plan = pending.plan();
818 let size_bytes = plan.expected_part_size(part_number)?;
819 let sha256 = normalize_sha256(&sha256)?;
820 let signed = self
821 .signer
822 .sign_multipart_part(SignMultipartPart {
823 key: pending.key.clone(),
824 upload_id: pending.provider_upload_id.clone(),
825 part_number,
826 size_bytes,
827 sha256: sha256.clone(),
828 expires_in: self.policy.part_expires_in,
829 })
830 .await?;
831 Ok(IssuedMultipartPart {
832 grant: MultipartPartGrant {
833 upload_id: pending.upload_id,
834 part_number,
835 size_bytes,
836 request: signed,
837 },
838 expected: ExpectedMultipartPart {
839 upload_id: pending.upload_id,
840 part_number,
841 size_bytes,
842 sha256,
843 },
844 })
845 }
846
847 pub fn accept_part(
848 &self,
849 pending: &PendingMultipartUpload,
850 expected: &ExpectedMultipartPart,
851 receipt: MultipartPartReceipt,
852 ) -> Result<TrustedMultipartPart, ObjectTransferError> {
853 self.validate_pending(pending)?;
854 if expected.upload_id != pending.upload_id
855 || receipt.part_number != expected.part_number
856 || normalize_sha256(&receipt.sha256)? != expected.sha256
857 || pending.plan().expected_part_size(receipt.part_number)? != expected.size_bytes
858 {
859 return Err(ObjectTransferError::PartReceiptMismatch);
860 }
861 validate_entity_tag(&receipt.entity_tag)?;
862 Ok(TrustedMultipartPart {
863 part_number: receipt.part_number,
864 size_bytes: expected.size_bytes,
865 entity_tag: receipt.entity_tag,
866 sha256: expected.sha256.clone(),
867 })
868 }
869
870 pub async fn complete(
871 &self,
872 pending: &PendingMultipartUpload,
873 parts: &[TrustedMultipartPart],
874 ) -> Result<CompletedMultipartObject, ObjectTransferError> {
875 self.validate_pending(pending)?;
876 validate_complete_parts(pending, parts)?;
877 let completed = self
878 .signer
879 .complete_multipart(CompleteMultipartObject {
880 key: pending.key.clone(),
881 upload_id: pending.provider_upload_id.clone(),
882 content_type: pending.expected_content_type.clone(),
883 size_bytes: pending.expected_size_bytes,
884 attributes: pending.expected_attributes.clone(),
885 parts: parts.to_vec(),
886 })
887 .await?;
888 if completed.key != pending.key {
889 return Err(ObjectTransferError::CompletedObjectMismatch);
890 }
891 if completed.content_type != pending.expected_content_type
892 || completed.size_bytes != pending.expected_size_bytes
893 || completed.attributes != pending.expected_attributes
894 {
895 return Err(ObjectTransferError::CompletedObjectMismatch);
896 }
897 Ok(completed)
898 }
899
900 pub async fn abort(&self, pending: &PendingMultipartUpload) -> Result<(), ObjectTransferError> {
901 self.validate_pending_identity(pending)?;
902 self.signer
903 .abort_multipart(&pending.key, &pending.provider_upload_id)
904 .await
905 }
906
907 fn validate_pending(
908 &self,
909 pending: &PendingMultipartUpload,
910 ) -> Result<(), ObjectTransferError> {
911 self.validate_pending_identity(pending)?;
912 let content_type = normalize_content_type(&pending.expected_content_type)?;
913 let plan = self.policy.plan(pending.expected_size_bytes)?;
914 let mut attributes = pending.expected_attributes.clone();
915 let upload_id = attributes.remove(UPLOAD_ID_ATTRIBUTE);
916 let expected_upload_id = pending.upload_id.to_string();
917 validate_attributes(attributes)?;
918 if content_type != pending.expected_content_type
919 || !self.policy.allowed_content_types.contains(&content_type)
920 || pending.part_size_bytes != plan.part_size_bytes
921 || pending.part_count != plan.part_count
922 || upload_id.as_deref() != Some(expected_upload_id.as_str())
923 {
924 return Err(ObjectTransferError::InvalidPendingUpload);
925 }
926 Ok(())
927 }
928
929 fn validate_pending_identity(
930 &self,
931 pending: &PendingMultipartUpload,
932 ) -> Result<(), ObjectTransferError> {
933 let expected_key = ObjectKey::parse(format!(
934 "{}/{}",
935 self.policy.key_prefix.as_str(),
936 pending.upload_id
937 ))?;
938 if pending.key != expected_key {
939 return Err(ObjectTransferError::InvalidPendingUpload);
940 }
941 Ok(())
942 }
943}
944
945impl PendingMultipartUpload {
946 const fn plan(&self) -> MultipartUploadPlan {
947 MultipartUploadPlan {
948 size_bytes: self.expected_size_bytes,
949 part_size_bytes: self.part_size_bytes,
950 part_count: self.part_count,
951 }
952 }
953}
954
955fn validate_complete_parts(
956 pending: &PendingMultipartUpload,
957 parts: &[TrustedMultipartPart],
958) -> Result<(), ObjectTransferError> {
959 if parts.len() != usize::try_from(pending.part_count).unwrap_or(usize::MAX) {
960 return Err(ObjectTransferError::IncompletePartManifest {
961 expected: pending.part_count,
962 actual: u32::try_from(parts.len()).unwrap_or(u32::MAX),
963 });
964 }
965 for (index, part) in parts.iter().enumerate() {
966 let part_number =
967 u32::try_from(index + 1).map_err(|_| ObjectTransferError::TooManyParts)?;
968 if part.part_number != part_number
969 || part.size_bytes != pending.plan().expected_part_size(part_number)?
970 {
971 return Err(ObjectTransferError::InvalidPartManifest);
972 }
973 normalize_sha256(&part.sha256)?;
974 validate_entity_tag(&part.entity_tag)?;
975 }
976 Ok(())
977}
978
979#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
982#[serde(tag = "state", rename_all = "snake_case")]
983pub enum ObjectValidationState {
984 Quarantined,
985 Accepted {
986 inspector: String,
987 inspected_at: DateTime<Utc>,
988 },
989 Rejected {
990 inspector: String,
991 code: String,
992 inspected_at: DateTime<Utc>,
993 },
994}
995
996#[derive(Debug, Clone, PartialEq, Eq)]
997pub enum ObjectInspectionVerdict {
998 Accept,
999 Reject { code: String },
1000}
1001
1002#[async_trait]
1003pub trait ObjectContentInspector: Send + Sync + fmt::Debug {
1004 fn id(&self) -> &str;
1005 async fn inspect(
1006 &self,
1007 object: &ObjectReadHead,
1008 ) -> Result<ObjectInspectionVerdict, ObjectTransferError>;
1009}
1010
1011pub async fn inspect_quarantined_object(
1012 inspector: &dyn ObjectContentInspector,
1013 object: &ObjectReadHead,
1014 now: DateTime<Utc>,
1015) -> Result<ObjectValidationState, ObjectTransferError> {
1016 let id = validate_inspector_id(inspector.id())?;
1017 match inspector.inspect(object).await? {
1018 ObjectInspectionVerdict::Accept => Ok(ObjectValidationState::Accepted {
1019 inspector: id,
1020 inspected_at: now,
1021 }),
1022 ObjectInspectionVerdict::Reject { code } => {
1023 let code = validate_inspection_code(&code)?;
1024 Ok(ObjectValidationState::Rejected {
1025 inspector: id,
1026 code,
1027 inspected_at: now,
1028 })
1029 }
1030 }
1031}
1032
1033#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1036pub struct ObjectTransferCostUsage {
1037 pub retained_bytes: u64,
1038 pub incomplete_multipart_bytes: u64,
1039 pub single_upload_requests: u64,
1040 pub multipart_initiations: u64,
1041 pub multipart_part_attempts: u64,
1042 pub multipart_completions: u64,
1043 pub multipart_aborts: u64,
1044 pub metadata_requests: u64,
1045 pub download_requests: u64,
1046 pub downloaded_bytes: u64,
1047 pub accelerated_bytes: u64,
1048 pub edge_requests: u64,
1049 pub edge_egress_bytes: u64,
1050}
1051
1052#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1053pub struct ObjectTransferCostProjection {
1054 pub complete: bool,
1055 pub fixed_compute: bool,
1056 pub api_relay_bytes: u64,
1057 pub usage: ObjectTransferCostUsage,
1058 pub missing_rates: Vec<String>,
1059 pub notes: Vec<String>,
1060}
1061
1062#[must_use]
1063pub fn estimate_object_transfer_cost(
1064 usage: ObjectTransferCostUsage,
1065) -> ObjectTransferCostProjection {
1066 let mut missing_rates = Vec::new();
1067 if usage.retained_bytes > 0 || usage.incomplete_multipart_bytes > 0 {
1068 missing_rates.push("storage_byte_month".into());
1069 }
1070 if usage.single_upload_requests
1071 + usage.multipart_initiations
1072 + usage.multipart_part_attempts
1073 + usage.multipart_completions
1074 + usage.multipart_aborts
1075 + usage.metadata_requests
1076 + usage.download_requests
1077 > 0
1078 {
1079 missing_rates.push("provider_request".into());
1080 }
1081 if usage.downloaded_bytes > 0 {
1082 missing_rates.push("provider_egress_byte".into());
1083 }
1084 if usage.accelerated_bytes > 0 {
1085 missing_rates.push("acceleration_byte".into());
1086 }
1087 if usage.edge_requests > 0 {
1088 missing_rates.push("edge_request".into());
1089 }
1090 if usage.edge_egress_bytes > 0 {
1091 missing_rates.push("edge_egress_byte".into());
1092 }
1093 ObjectTransferCostProjection {
1094 complete: missing_rates.is_empty(),
1095 fixed_compute: false,
1096 api_relay_bytes: 0,
1097 usage,
1098 missing_rates,
1099 notes: vec![
1100 "Direct transfer excludes Lambda and API Gateway file-body relay.".into(),
1101 "Incomplete multipart bytes accrue storage cost until completion, abort, or lifecycle cleanup.".into(),
1102 "A provider bill additionally requires account, Region, storage class, destination, retention, and current rates.".into(),
1103 ],
1104 }
1105}
1106
1107#[non_exhaustive]
1108#[derive(Debug, thiserror::Error)]
1109pub enum ObjectTransferError {
1110 #[error("object byte range is invalid")]
1111 InvalidRange,
1112 #[error("object byte range cannot be satisfied")]
1113 RangeNotSatisfiable,
1114 #[error("the requested object does not exist")]
1115 MissingObject,
1116 #[error("the object changed after the client observed it")]
1117 PreconditionFailed,
1118 #[error("download filename is invalid")]
1119 InvalidDownloadName,
1120 #[error("capability expiry must be greater than zero and no more than 24 hours")]
1121 InvalidExpiry,
1122 #[error("multipart maximum size is invalid")]
1123 InvalidMaximumSize,
1124 #[error("multipart part size is invalid")]
1125 InvalidPartSize,
1126 #[error("multipart upload would exceed the provider part limit")]
1127 TooManyParts,
1128 #[error("multipart part number is invalid")]
1129 InvalidPartNumber,
1130 #[error("upload content type is invalid")]
1131 InvalidContentType,
1132 #[error("upload policy must allow at least one content type")]
1133 EmptyContentTypeAllowlist,
1134 #[error("upload content type is not allowed: {0}")]
1135 UnsupportedContentType(String),
1136 #[error("upload body must not be empty")]
1137 EmptyObject,
1138 #[error("the requested upload is {actual} bytes; the maximum is {maximum} bytes")]
1139 ObjectTooLarge { actual: u64, maximum: u64 },
1140 #[error("multipart part SHA-256 must be exactly 64 hexadecimal characters")]
1141 InvalidSha256,
1142 #[error("multipart upload attributes are invalid or reserved")]
1143 InvalidAttributes,
1144 #[error("provider multipart upload ID is invalid")]
1145 InvalidProviderUploadId,
1146 #[error("persisted multipart upload state does not match its configured policy")]
1147 InvalidPendingUpload,
1148 #[error("multipart part entity tag is invalid")]
1149 InvalidEntityTag,
1150 #[error("multipart part receipt does not match the issued part")]
1151 PartReceiptMismatch,
1152 #[error("multipart manifest is incomplete: expected {expected} parts, received {actual}")]
1153 IncompletePartManifest { expected: u32, actual: u32 },
1154 #[error("multipart manifest is not consecutive and exact")]
1155 InvalidPartManifest,
1156 #[error("provider completed object metadata does not match the trusted session")]
1157 CompletedObjectMismatch,
1158 #[error("content inspector identity or rejection code is invalid")]
1159 InvalidInspectionResult,
1160 #[error(transparent)]
1161 ObjectStore(#[from] ObjectStoreError),
1162 #[error("object transfer provider failed: {0}")]
1163 Provider(String),
1164}
1165
1166fn validate_expiry(value: TimeDelta) -> Result<(), ObjectTransferError> {
1167 if value <= TimeDelta::zero() || value > TimeDelta::seconds(MAX_CAPABILITY_EXPIRY_SECONDS) {
1168 Err(ObjectTransferError::InvalidExpiry)
1169 } else {
1170 Ok(())
1171 }
1172}
1173
1174fn validate_download_name(value: &str) -> Result<(), ObjectTransferError> {
1175 if value.is_empty()
1176 || value.len() > 255
1177 || !value
1178 .bytes()
1179 .all(|byte| byte == b' ' || byte.is_ascii_graphic())
1180 || value
1181 .chars()
1182 .any(|character| matches!(character, '"' | '\\' | '/' | ';'))
1183 {
1184 Err(ObjectTransferError::InvalidDownloadName)
1185 } else {
1186 Ok(())
1187 }
1188}
1189
1190fn normalize_content_type(value: &str) -> Result<String, ObjectTransferError> {
1191 let value = value.trim().to_ascii_lowercase();
1192 let Some((top, subtype)) = value.split_once('/') else {
1193 return Err(ObjectTransferError::InvalidContentType);
1194 };
1195 if value.len() > 255
1196 || subtype.contains('/')
1197 || !valid_media_token(top)
1198 || !valid_media_token(subtype)
1199 {
1200 Err(ObjectTransferError::InvalidContentType)
1201 } else {
1202 Ok(value)
1203 }
1204}
1205
1206fn valid_media_token(value: &str) -> bool {
1207 !value.is_empty()
1208 && value.bytes().all(|byte| {
1209 byte.is_ascii_alphanumeric()
1210 || matches!(
1211 byte,
1212 b'!' | b'#' | b'$' | b'&' | b'^' | b'_' | b'.' | b'+' | b'-'
1213 )
1214 })
1215}
1216
1217fn normalize_sha256(value: &str) -> Result<String, ObjectTransferError> {
1218 let value = value.trim();
1219 if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
1220 Err(ObjectTransferError::InvalidSha256)
1221 } else {
1222 Ok(value.to_ascii_lowercase())
1223 }
1224}
1225
1226fn validate_attributes(
1227 attributes: BTreeMap<String, String>,
1228) -> Result<BTreeMap<String, String>, ObjectTransferError> {
1229 if attributes.len() > 31
1230 || attributes.iter().any(|(key, value)| {
1231 key.trim().is_empty()
1232 || key.starts_with(RESERVED_ATTRIBUTE_PREFIX)
1233 || key.len() > 128
1234 || value.len() > 1_024
1235 || key.chars().any(char::is_control)
1236 || value.chars().any(char::is_control)
1237 })
1238 {
1239 Err(ObjectTransferError::InvalidAttributes)
1240 } else {
1241 Ok(attributes)
1242 }
1243}
1244
1245fn validate_entity_tag(value: &str) -> Result<(), ObjectTransferError> {
1246 if value.is_empty()
1247 || value.len() > crate::MAX_MULTIPART_ENTITY_TAG_BYTES
1248 || value.starts_with("W/")
1249 || value.chars().any(char::is_control)
1250 {
1251 Err(ObjectTransferError::InvalidEntityTag)
1252 } else {
1253 Ok(())
1254 }
1255}
1256
1257fn validate_inspector_id(value: &str) -> Result<String, ObjectTransferError> {
1258 if valid_identifier(value) {
1259 Ok(value.into())
1260 } else {
1261 Err(ObjectTransferError::InvalidInspectionResult)
1262 }
1263}
1264
1265fn validate_inspection_code(value: &str) -> Result<String, ObjectTransferError> {
1266 if valid_identifier(value) {
1267 Ok(value.into())
1268 } else {
1269 Err(ObjectTransferError::InvalidInspectionResult)
1270 }
1271}
1272
1273fn valid_identifier(value: &str) -> bool {
1274 !value.is_empty()
1275 && value.len() <= 128
1276 && value.bytes().all(|byte| {
1277 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_' | b'.')
1278 })
1279}
1280
1281fn transfer_to_store(error: &ObjectTransferError) -> ObjectStoreError {
1282 ObjectStoreError::Store(error.to_string())
1283}