use std::{collections::BTreeMap, fmt, num::NonZeroU16};
use http::HeaderMap;
use super::{
ByteRange, Checksum, ChecksumAlgorithm, ChecksumType, Conditions, ObjectKey, RequestIds,
};
use crate::stream::{ByteStream, ResponseStream};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ObjectMetadata {
pub e_tag: Option<String>,
pub content_length: u64,
pub content_type: Option<String>,
pub last_modified: Option<time::OffsetDateTime>,
pub version_id: Option<String>,
pub user_metadata: BTreeMap<String, String>,
pub checksum: Checksum,
pub request_ids: RequestIds,
}
#[derive(derive_more::Debug)]
pub struct PutObjectRequest {
pub key: ObjectKey,
#[debug("{:?}", "<stream>")]
pub body: ByteStream,
pub content_type: Option<String>,
#[debug("{:?}", self.user_metadata.keys().collect::<Vec<_>>())]
pub user_metadata: BTreeMap<String, String>,
pub conditions: Conditions,
pub checksum_algorithm: Option<ChecksumAlgorithm>,
#[debug("{:?}", "<redacted>")]
pub headers: HeaderMap,
}
impl PutObjectRequest {
pub fn new(key: ObjectKey, body: ByteStream) -> Self {
Self {
key,
body,
content_type: None,
user_metadata: BTreeMap::new(),
conditions: Conditions::default(),
checksum_algorithm: None,
headers: HeaderMap::new(),
}
}
pub fn with_headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PutObjectOutput {
pub e_tag: Option<String>,
pub version_id: Option<String>,
pub checksum: Checksum,
pub request_ids: RequestIds,
}
#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
pub struct GetObjectRequest {
pub key: ObjectKey,
pub range: Option<ByteRange>,
pub conditions: Conditions,
pub version_id: Option<String>,
#[debug("{:?}", "<redacted>")]
pub headers: HeaderMap,
}
impl GetObjectRequest {
pub fn new(key: ObjectKey) -> Self {
Self {
key,
range: None,
conditions: Conditions::default(),
version_id: None,
headers: HeaderMap::new(),
}
}
pub fn with_headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
}
pub struct GetObjectOutput {
pub metadata: ObjectMetadata,
pub body: ResponseStream,
pub content_range: Option<(u64, u64, Option<u64>)>,
}
impl fmt::Debug for GetObjectOutput {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("GetObjectOutput")
.field("metadata", &self.metadata)
.field("body", &"<stream>")
.field("content_range", &self.content_range)
.finish()
}
}
#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
pub struct HeadObjectRequest {
pub key: ObjectKey,
pub conditions: Conditions,
pub version_id: Option<String>,
#[debug("{:?}", "<redacted>")]
pub headers: HeaderMap,
}
impl HeadObjectRequest {
pub fn new(key: ObjectKey) -> Self {
Self {
key,
conditions: Conditions::default(),
version_id: None,
headers: HeaderMap::new(),
}
}
pub fn with_headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
}
pub type HeadObjectOutput = ObjectMetadata;
#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
pub struct DeleteObjectRequest {
pub key: ObjectKey,
pub version_id: Option<String>,
pub if_match: Option<String>,
#[debug("{:?}", "<redacted>")]
pub headers: HeaderMap,
}
impl DeleteObjectRequest {
pub fn new(key: ObjectKey) -> Self {
Self {
key,
version_id: None,
if_match: None,
headers: HeaderMap::new(),
}
}
pub fn with_headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DeleteObjectOutput {
pub delete_marker: bool,
pub version_id: Option<String>,
pub request_ids: RequestIds,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeleteObjectIdentifier {
pub key: ObjectKey,
pub version_id: Option<String>,
pub if_match: Option<String>,
}
impl DeleteObjectIdentifier {
pub fn new(key: ObjectKey) -> Self {
Self {
key,
version_id: None,
if_match: None,
}
}
}
pub const MAX_DELETE_OBJECTS: usize = 1_000;
#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
pub struct DeleteObjectsRequest {
objects: Vec<DeleteObjectIdentifier>,
pub quiet: bool,
#[debug("{:?}", "<redacted>")]
pub headers: HeaderMap,
}
impl DeleteObjectsRequest {
pub fn new(objects: Vec<DeleteObjectIdentifier>) -> Result<Self, DeleteObjectsError> {
if objects.is_empty() {
return Err(DeleteObjectsError::Empty);
}
if objects.len() > MAX_DELETE_OBJECTS {
return Err(DeleteObjectsError::TooMany {
actual: objects.len(),
maximum: MAX_DELETE_OBJECTS,
});
}
Ok(Self {
objects,
quiet: false,
headers: HeaderMap::new(),
})
}
pub fn with_headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
pub fn objects(&self) -> &[DeleteObjectIdentifier] {
&self.objects
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
pub enum DeleteObjectsError {
#[error("a multi-object delete request cannot be empty")]
Empty,
#[error("multi-object delete has {actual} entries; the maximum is {maximum}")]
TooMany {
actual: usize,
maximum: usize,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeletedObject {
pub key: ObjectKey,
pub version_id: Option<String>,
pub delete_marker: bool,
pub delete_marker_version_id: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeleteError {
pub key: String,
pub version_id: Option<String>,
pub code: Option<String>,
pub message: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DeleteObjectsOutput {
pub deleted: Vec<DeletedObject>,
pub errors: Vec<DeleteError>,
pub request_ids: RequestIds,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CopySource {
pub bucket: String,
pub key: ObjectKey,
pub version_id: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum CopyMetadataDirective {
#[default]
Copy,
Replace {
content_type: Option<String>,
user_metadata: BTreeMap<String, String>,
},
}
#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
pub struct CopyObjectRequest {
pub source: CopySource,
pub destination: ObjectKey,
pub source_conditions: Conditions,
#[debug(
"{:?}",
match self.metadata {
CopyMetadataDirective::Copy => "copy",
CopyMetadataDirective::Replace { .. } => "replace",
}
)]
pub metadata: CopyMetadataDirective,
#[debug("{:?}", "<redacted>")]
pub headers: HeaderMap,
}
impl CopyObjectRequest {
pub fn new(source: CopySource, destination: ObjectKey) -> Self {
Self {
source,
destination,
source_conditions: Conditions::default(),
metadata: CopyMetadataDirective::Copy,
headers: HeaderMap::new(),
}
}
pub fn with_headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ObjectOwner {
pub id: Option<String>,
pub display_name: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CopyObjectOutput {
pub e_tag: Option<String>,
pub last_modified: Option<time::OffsetDateTime>,
pub version_id: Option<String>,
pub checksum: Checksum,
pub request_ids: RequestIds,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PageSize(NonZeroU16);
impl PageSize {
pub const MAX: u16 = 1_000;
pub fn new(value: u16) -> Option<Self> {
NonZeroU16::new(value)
.filter(|value| value.get() <= Self::MAX)
.map(Self)
}
pub const fn get(self) -> u16 {
self.0.get()
}
}
impl Default for PageSize {
fn default() -> Self {
Self(NonZeroU16::new(Self::MAX).expect("page-size maximum must be non-zero"))
}
}
#[derive(Clone, Default, derive_more::Debug, Eq, PartialEq)]
pub struct ListObjectsV2Request {
pub prefix: Option<String>,
pub delimiter: Option<String>,
pub continuation_token: Option<String>,
pub start_after: Option<ObjectKey>,
pub max_keys: PageSize,
pub fetch_owner: bool,
#[debug("{:?}", "<redacted>")]
pub headers: HeaderMap,
}
impl ListObjectsV2Request {
pub fn with_headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ListedObject {
pub key: ObjectKey,
pub last_modified: Option<time::OffsetDateTime>,
pub e_tag: Option<String>,
pub size: u64,
pub storage_class: Option<String>,
pub owner: Option<ObjectOwner>,
pub checksum_algorithms: Vec<String>,
pub checksum_type: Option<ChecksumType>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ListObjectsV2Output {
pub objects: Vec<ListedObject>,
pub common_prefixes: Vec<String>,
pub is_truncated: bool,
pub next_continuation_token: Option<String>,
pub key_count: Option<u32>,
pub request_ids: RequestIds,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn delete_batches_are_strictly_bounded() {
assert_eq!(
DeleteObjectsRequest::new(Vec::new()).unwrap_err(),
DeleteObjectsError::Empty
);
let key = ObjectKey::new("key").unwrap();
let too_many = (0..=MAX_DELETE_OBJECTS)
.map(|_| DeleteObjectIdentifier::new(key.clone()))
.collect();
assert!(matches!(
DeleteObjectsRequest::new(too_many),
Err(DeleteObjectsError::TooMany { .. })
));
}
#[test]
fn custom_header_values_are_redacted() {
let mut request = GetObjectRequest::new(ObjectKey::new("key").unwrap());
request.headers.insert(
"x-amz-server-side-encryption-customer-key",
"sentinel-sse-c-key".parse().unwrap(),
);
let debug = format!("{request:?}");
assert!(debug.contains(r#"headers: "<redacted>""#));
assert!(!debug.contains("sentinel-sse-c-key"));
}
#[test]
fn page_sizes_reject_zero_and_values_over_service_limit() {
assert!(PageSize::new(0).is_none());
assert_eq!(PageSize::new(1).unwrap().get(), 1);
assert_eq!(PageSize::new(1_000).unwrap().get(), 1_000);
assert!(PageSize::new(1_001).is_none());
}
}