Skip to main content

s3_wire/operation/multipart/
completion.rs

1use http::HeaderMap;
2
3use super::{CompletedPart, PartNumber, UploadId};
4use crate::operation::{Checksum, ObjectKey, RequestIds};
5
6/// Request to complete a multipart upload.
7#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
8pub struct CompleteMultipartUploadRequest {
9    /// Destination object key.
10    pub key: ObjectKey,
11    upload_id: UploadId,
12    completed_parts: Vec<CompletedPart>,
13    /// Additional request headers. Values are signed and repeated values are preserved.
14    /// Generated-name collisions are errors; signing- and transport-owned headers are rejected.
15    #[debug("{:?}", "<redacted>")]
16    pub headers: HeaderMap,
17}
18
19impl CompleteMultipartUploadRequest {
20    /// Validates that the upload ID is present and parts are non-empty, unique,
21    /// and strictly ascending.
22    pub fn new(
23        key: ObjectKey,
24        upload_id: UploadId,
25        completed_parts: Vec<CompletedPart>,
26    ) -> Result<Self, MultipartError> {
27        validate_completed_parts(&completed_parts)?;
28        Ok(Self {
29            key,
30            upload_id,
31            completed_parts,
32            headers: HeaderMap::new(),
33        })
34    }
35
36    /// Replaces the request's additional headers.
37    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
38        self.headers = headers;
39        self
40    }
41
42    /// Returns the validated, ordered completed parts.
43    pub fn completed_parts(&self) -> &[CompletedPart] {
44        &self.completed_parts
45    }
46
47    /// Returns the validated, non-empty upload identifier.
48    pub const fn upload_id(&self) -> &UploadId {
49        &self.upload_id
50    }
51}
52
53/// Result of completing a multipart upload.
54#[derive(Clone, Debug, Default, Eq, PartialEq)]
55pub struct CompleteMultipartUploadOutput {
56    /// Service-provided object location. This is not exposed as a signed URL.
57    pub location: Option<String>,
58    /// Bucket reported by the service.
59    pub bucket: Option<String>,
60    /// Completed object key.
61    pub key: Option<ObjectKey>,
62    /// Entity tag of the completed object.
63    pub e_tag: Option<String>,
64    /// Version identifier, when enabled.
65    pub version_id: Option<String>,
66    /// Checksums returned by the service.
67    pub checksum: Checksum,
68    /// Service request identifiers.
69    pub request_ids: RequestIds,
70}
71
72/// Request to abort an in-progress multipart upload.
73#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
74pub struct AbortMultipartUploadRequest {
75    key: ObjectKey,
76    upload_id: UploadId,
77    /// Additional request headers. Values are signed and repeated values are preserved.
78    /// Generated-name collisions are errors; signing- and transport-owned headers are rejected.
79    #[debug("{:?}", "<redacted>")]
80    pub headers: HeaderMap,
81}
82
83impl AbortMultipartUploadRequest {
84    /// Constructs an abort request from a validated upload identifier.
85    pub fn new(key: ObjectKey, upload_id: UploadId) -> Self {
86        Self {
87            key,
88            upload_id,
89            headers: HeaderMap::new(),
90        }
91    }
92
93    /// Replaces the request's additional headers.
94    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
95        self.headers = headers;
96        self
97    }
98
99    /// Returns the destination object key.
100    pub const fn key(&self) -> &ObjectKey {
101        &self.key
102    }
103
104    /// Returns the validated multipart upload identifier.
105    pub const fn upload_id(&self) -> &UploadId {
106        &self.upload_id
107    }
108}
109
110/// Invalid multipart state or descriptor.
111#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
112pub enum MultipartError {
113    /// A part number is outside S3's 1 through 10,000 range.
114    #[error("multipart part number {0} is outside 1..=10000")]
115    InvalidPartNumber(u16),
116    /// An uploaded part has no usable entity tag.
117    #[error("multipart part {part_number} has an empty ETag")]
118    EmptyETag {
119        /// Part with the invalid entity tag.
120        part_number: u16,
121    },
122    /// A completed part number was recorded more than once.
123    #[error("multipart part {0:?} was recorded more than once")]
124    DuplicatePart(PartNumber),
125    /// Completion was attempted without any completed parts.
126    #[error("a multipart upload cannot be completed without parts")]
127    NoCompletedParts,
128    /// Completed parts are duplicated or out of ascending order.
129    #[error("multipart part {next:?} does not follow {previous:?} in ascending order")]
130    OutOfOrder {
131        /// Last accepted part number.
132        previous: PartNumber,
133        /// Rejected part number.
134        next: PartNumber,
135    },
136}
137
138fn validate_completed_parts(parts: &[CompletedPart]) -> Result<(), MultipartError> {
139    if parts.is_empty() {
140        return Err(MultipartError::NoCompletedParts);
141    }
142    for window in parts.windows(2) {
143        if window[1].part_number() <= window[0].part_number() {
144            return Err(MultipartError::OutOfOrder {
145                previous: window[0].part_number(),
146                next: window[1].part_number(),
147            });
148        }
149    }
150    Ok(())
151}