use std::{collections::HashMap, fmt, str::FromStr};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Owner {
pub id: String,
pub display_name: String,
}
impl Default for Owner {
fn default() -> Self {
Self {
id: "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a".to_owned(),
display_name: "webfile".to_owned(),
}
}
}
impl fmt::Display for Owner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", self.display_name, self.id)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum CannedAcl {
#[default]
Private,
PublicRead,
PublicReadWrite,
AuthenticatedRead,
AwsExecRead,
BucketOwnerRead,
BucketOwnerFullControl,
LogDeliveryWrite,
}
impl CannedAcl {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Private => "private",
Self::PublicRead => "public-read",
Self::PublicReadWrite => "public-read-write",
Self::AuthenticatedRead => "authenticated-read",
Self::AwsExecRead => "aws-exec-read",
Self::BucketOwnerRead => "bucket-owner-read",
Self::BucketOwnerFullControl => "bucket-owner-full-control",
Self::LogDeliveryWrite => "log-delivery-write",
}
}
}
impl fmt::Display for CannedAcl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("unknown canned ACL: {0}")]
pub struct ParseCannedAclError(String);
impl FromStr for CannedAcl {
type Err = ParseCannedAclError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"private" => Ok(Self::Private),
"public-read" => Ok(Self::PublicRead),
"public-read-write" => Ok(Self::PublicReadWrite),
"authenticated-read" => Ok(Self::AuthenticatedRead),
"aws-exec-read" => Ok(Self::AwsExecRead),
"bucket-owner-read" => Ok(Self::BucketOwnerRead),
"bucket-owner-full-control" => Ok(Self::BucketOwnerFullControl),
"log-delivery-write" => Ok(Self::LogDeliveryWrite),
_ => Err(ParseCannedAclError(s.to_owned())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Grant {
pub grantee: Grantee,
pub permission: Permission,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum Grantee {
CanonicalUser {
id: String,
display_name: String,
},
Group {
uri: String,
},
Email {
email: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Permission {
FullControl,
Read,
Write,
ReadAcp,
WriteAcp,
}
impl fmt::Display for Permission {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::FullControl => "FULL_CONTROL",
Self::Read => "READ",
Self::Write => "WRITE",
Self::ReadAcp => "READ_ACP",
Self::WriteAcp => "WRITE_ACP",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChecksumData {
pub algorithm: String,
pub value: String,
#[serde(default = "default_checksum_type")]
pub checksum_type: String,
}
fn default_checksum_type() -> String {
"FULL_OBJECT".to_owned()
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ObjectMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_encoding: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_disposition: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_language: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires: Option<String>,
#[serde(default)]
pub user_metadata: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sse_algorithm: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sse_kms_key_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sse_bucket_key_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sse_customer_algorithm: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sse_customer_key_md5: Option<String>,
#[serde(default)]
pub tagging: Vec<(String, String)>,
#[serde(default)]
pub acl: CannedAcl,
#[serde(skip_serializing_if = "Option::is_none")]
pub object_lock_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object_lock_retain_until: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object_lock_legal_hold: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct S3Object {
pub key: String,
pub version_id: String,
pub etag: String,
pub size: u64,
pub last_modified: DateTime<Utc>,
pub storage_class: String,
pub metadata: ObjectMetadata,
pub owner: Owner,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum: Option<ChecksumData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parts_count: Option<u32>,
#[serde(default)]
pub part_etags: Vec<String>,
}
impl S3Object {
#[must_use]
pub fn is_delete_marker(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct S3DeleteMarker {
pub key: String,
pub version_id: String,
pub last_modified: DateTime<Utc>,
pub owner: Owner,
}
impl S3DeleteMarker {
#[must_use]
pub fn is_delete_marker(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum ObjectVersion {
Object(Box<S3Object>),
DeleteMarker(S3DeleteMarker),
}
impl ObjectVersion {
#[must_use]
pub fn key(&self) -> &str {
match self {
Self::Object(obj) => &obj.key,
Self::DeleteMarker(dm) => &dm.key,
}
}
#[must_use]
pub fn version_id(&self) -> &str {
match self {
Self::Object(obj) => &obj.version_id,
Self::DeleteMarker(dm) => &dm.version_id,
}
}
#[must_use]
pub fn last_modified(&self) -> DateTime<Utc> {
match self {
Self::Object(obj) => obj.last_modified,
Self::DeleteMarker(dm) => dm.last_modified,
}
}
#[must_use]
pub fn is_delete_marker(&self) -> bool {
matches!(self, Self::DeleteMarker(_))
}
#[must_use]
pub fn owner(&self) -> &Owner {
match self {
Self::Object(obj) => &obj.owner,
Self::DeleteMarker(dm) => &dm.owner,
}
}
#[must_use]
pub fn as_object(&self) -> Option<&S3Object> {
match self {
Self::Object(obj) => Some(obj),
Self::DeleteMarker(_) => None,
}
}
pub fn as_object_mut(&mut self) -> Option<&mut S3Object> {
match self {
Self::Object(obj) => Some(obj),
Self::DeleteMarker(_) => None,
}
}
#[must_use]
pub fn as_delete_marker(&self) -> Option<&S3DeleteMarker> {
match self {
Self::Object(_) => None,
Self::DeleteMarker(dm) => Some(dm),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_use_default_owner() {
let owner = Owner::default();
assert_eq!(owner.display_name, "webfile");
assert!(!owner.id.is_empty());
}
#[test]
fn test_should_display_owner() {
let owner = Owner {
id: "abc123".to_owned(),
display_name: "alice".to_owned(),
};
assert_eq!(format!("{owner}"), "alice(abc123)");
}
#[test]
fn test_should_default_canned_acl_to_private() {
assert_eq!(CannedAcl::default(), CannedAcl::Private);
assert_eq!(CannedAcl::default().as_str(), "private");
}
#[test]
fn test_should_roundtrip_canned_acl_from_str() {
let cases = [
("private", CannedAcl::Private),
("public-read", CannedAcl::PublicRead),
("public-read-write", CannedAcl::PublicReadWrite),
("authenticated-read", CannedAcl::AuthenticatedRead),
("aws-exec-read", CannedAcl::AwsExecRead),
("bucket-owner-read", CannedAcl::BucketOwnerRead),
(
"bucket-owner-full-control",
CannedAcl::BucketOwnerFullControl,
),
("log-delivery-write", CannedAcl::LogDeliveryWrite),
];
for (s, expected) in cases {
let parsed: CannedAcl = s.parse().unwrap_or_else(|_| panic!("failed to parse {s}"));
assert_eq!(parsed, expected);
assert_eq!(parsed.as_str(), s);
}
}
#[test]
fn test_should_reject_unknown_canned_acl() {
let result = "unknown-acl".parse::<CannedAcl>();
assert!(result.is_err());
}
#[test]
fn test_should_identify_object_as_not_delete_marker() {
let obj = make_test_object("test-key");
assert!(!obj.is_delete_marker());
}
#[test]
fn test_should_identify_delete_marker() {
let dm = S3DeleteMarker {
key: "test-key".to_owned(),
version_id: "v1".to_owned(),
last_modified: Utc::now(),
owner: Owner::default(),
};
assert!(dm.is_delete_marker());
}
#[test]
fn test_should_access_object_version_fields() {
let obj = make_test_object("my-key");
let version = ObjectVersion::Object(Box::new(obj));
assert_eq!(version.key(), "my-key");
assert_eq!(version.version_id(), "null");
assert!(!version.is_delete_marker());
assert!(version.as_object().is_some());
assert!(version.as_delete_marker().is_none());
}
#[test]
fn test_should_access_delete_marker_version_fields() {
let dm = S3DeleteMarker {
key: "deleted-key".to_owned(),
version_id: "dm-v1".to_owned(),
last_modified: Utc::now(),
owner: Owner::default(),
};
let version = ObjectVersion::DeleteMarker(dm);
assert_eq!(version.key(), "deleted-key");
assert_eq!(version.version_id(), "dm-v1");
assert!(version.is_delete_marker());
assert!(version.as_object().is_none());
assert!(version.as_delete_marker().is_some());
}
#[test]
fn test_should_default_object_metadata() {
let meta = ObjectMetadata::default();
assert!(meta.content_type.is_none());
assert!(meta.user_metadata.is_empty());
assert!(meta.tagging.is_empty());
assert_eq!(meta.acl, CannedAcl::Private);
assert!(meta.object_lock_mode.is_none());
}
#[test]
fn test_should_display_permission() {
assert_eq!(format!("{}", Permission::FullControl), "FULL_CONTROL");
assert_eq!(format!("{}", Permission::Read), "READ");
assert_eq!(format!("{}", Permission::Write), "WRITE");
assert_eq!(format!("{}", Permission::ReadAcp), "READ_ACP");
assert_eq!(format!("{}", Permission::WriteAcp), "WRITE_ACP");
}
fn make_test_object(key: &str) -> S3Object {
S3Object {
key: key.to_owned(),
version_id: "null".to_owned(),
etag: "\"d41d8cd98f00b204e9800998ecf8427e\"".to_owned(),
size: 0,
last_modified: Utc::now(),
storage_class: "STANDARD".to_owned(),
metadata: ObjectMetadata::default(),
owner: Owner::default(),
checksum: None,
parts_count: None,
part_etags: Vec::new(),
}
}
}