1use std::{collections::HashMap, fmt, str::FromStr};
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct Owner {
19 pub id: String,
21 pub display_name: String,
23}
24
25impl Default for Owner {
26 fn default() -> Self {
27 Self {
28 id: "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a".to_owned(),
29 display_name: "webfile".to_owned(),
30 }
31 }
32}
33
34impl fmt::Display for Owner {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 write!(f, "{}({})", self.display_name, self.id)
37 }
38}
39
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
46pub enum CannedAcl {
47 #[default]
49 Private,
50 PublicRead,
52 PublicReadWrite,
54 AuthenticatedRead,
56 AwsExecRead,
59 BucketOwnerRead,
61 BucketOwnerFullControl,
63 LogDeliveryWrite,
65}
66
67impl CannedAcl {
68 #[must_use]
70 pub fn as_str(&self) -> &str {
71 match self {
72 Self::Private => "private",
73 Self::PublicRead => "public-read",
74 Self::PublicReadWrite => "public-read-write",
75 Self::AuthenticatedRead => "authenticated-read",
76 Self::AwsExecRead => "aws-exec-read",
77 Self::BucketOwnerRead => "bucket-owner-read",
78 Self::BucketOwnerFullControl => "bucket-owner-full-control",
79 Self::LogDeliveryWrite => "log-delivery-write",
80 }
81 }
82}
83
84impl fmt::Display for CannedAcl {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.write_str(self.as_str())
87 }
88}
89
90#[derive(Debug, Clone, thiserror::Error)]
92#[error("unknown canned ACL: {0}")]
93pub struct ParseCannedAclError(String);
94
95impl FromStr for CannedAcl {
96 type Err = ParseCannedAclError;
97
98 fn from_str(s: &str) -> Result<Self, Self::Err> {
99 match s {
100 "private" => Ok(Self::Private),
101 "public-read" => Ok(Self::PublicRead),
102 "public-read-write" => Ok(Self::PublicReadWrite),
103 "authenticated-read" => Ok(Self::AuthenticatedRead),
104 "aws-exec-read" => Ok(Self::AwsExecRead),
105 "bucket-owner-read" => Ok(Self::BucketOwnerRead),
106 "bucket-owner-full-control" => Ok(Self::BucketOwnerFullControl),
107 "log-delivery-write" => Ok(Self::LogDeliveryWrite),
108 _ => Err(ParseCannedAclError(s.to_owned())),
109 }
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct Grant {
121 pub grantee: Grantee,
123 pub permission: Permission,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase", tag = "type")]
130pub enum Grantee {
131 CanonicalUser {
133 id: String,
135 display_name: String,
137 },
138 Group {
140 uri: String,
143 },
144 Email {
146 email: String,
148 },
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153pub enum Permission {
154 FullControl,
156 Read,
158 Write,
160 ReadAcp,
162 WriteAcp,
164}
165
166impl fmt::Display for Permission {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 let s = match self {
169 Self::FullControl => "FULL_CONTROL",
170 Self::Read => "READ",
171 Self::Write => "WRITE",
172 Self::ReadAcp => "READ_ACP",
173 Self::WriteAcp => "WRITE_ACP",
174 };
175 f.write_str(s)
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct ChecksumData {
187 pub algorithm: String,
189 pub value: String,
191 #[serde(default = "default_checksum_type")]
193 pub checksum_type: String,
194}
195
196fn default_checksum_type() -> String {
198 "FULL_OBJECT".to_owned()
199}
200
201#[derive(Debug, Clone, Default, Serialize, Deserialize)]
210#[serde(rename_all = "camelCase")]
211pub struct ObjectMetadata {
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub content_type: Option<String>,
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub content_encoding: Option<String>,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub content_disposition: Option<String>,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub content_language: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub cache_control: Option<String>,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub expires: Option<String>,
230 #[serde(default)]
232 pub user_metadata: HashMap<String, String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub sse_algorithm: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub sse_kms_key_id: Option<String>,
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub sse_bucket_key_enabled: Option<bool>,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub sse_customer_algorithm: Option<String>,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub sse_customer_key_md5: Option<String>,
248 #[serde(default)]
250 pub tagging: Vec<(String, String)>,
251 #[serde(default)]
253 pub acl: CannedAcl,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub object_lock_mode: Option<String>,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub object_lock_retain_until: Option<DateTime<Utc>>,
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub object_lock_legal_hold: Option<bool>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct S3Object {
273 pub key: String,
275 pub version_id: String,
277 pub etag: String,
279 pub size: u64,
281 pub last_modified: DateTime<Utc>,
283 pub storage_class: String,
285 pub metadata: ObjectMetadata,
287 pub owner: Owner,
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub checksum: Option<ChecksumData>,
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub parts_count: Option<u32>,
295 #[serde(default)]
297 pub part_etags: Vec<String>,
298}
299
300impl S3Object {
301 #[must_use]
303 pub fn is_delete_marker(&self) -> bool {
304 false
305 }
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
317#[serde(rename_all = "camelCase")]
318pub struct S3DeleteMarker {
319 pub key: String,
321 pub version_id: String,
323 pub last_modified: DateTime<Utc>,
325 pub owner: Owner,
327}
328
329impl S3DeleteMarker {
330 #[must_use]
332 pub fn is_delete_marker(&self) -> bool {
333 true
334 }
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase", tag = "type")]
345pub enum ObjectVersion {
346 Object(Box<S3Object>),
348 DeleteMarker(S3DeleteMarker),
350}
351
352impl ObjectVersion {
353 #[must_use]
355 pub fn key(&self) -> &str {
356 match self {
357 Self::Object(obj) => &obj.key,
358 Self::DeleteMarker(dm) => &dm.key,
359 }
360 }
361
362 #[must_use]
364 pub fn version_id(&self) -> &str {
365 match self {
366 Self::Object(obj) => &obj.version_id,
367 Self::DeleteMarker(dm) => &dm.version_id,
368 }
369 }
370
371 #[must_use]
373 pub fn last_modified(&self) -> DateTime<Utc> {
374 match self {
375 Self::Object(obj) => obj.last_modified,
376 Self::DeleteMarker(dm) => dm.last_modified,
377 }
378 }
379
380 #[must_use]
382 pub fn is_delete_marker(&self) -> bool {
383 matches!(self, Self::DeleteMarker(_))
384 }
385
386 #[must_use]
388 pub fn owner(&self) -> &Owner {
389 match self {
390 Self::Object(obj) => &obj.owner,
391 Self::DeleteMarker(dm) => &dm.owner,
392 }
393 }
394
395 #[must_use]
397 pub fn as_object(&self) -> Option<&S3Object> {
398 match self {
399 Self::Object(obj) => Some(obj),
400 Self::DeleteMarker(_) => None,
401 }
402 }
403
404 pub fn as_object_mut(&mut self) -> Option<&mut S3Object> {
406 match self {
407 Self::Object(obj) => Some(obj),
408 Self::DeleteMarker(_) => None,
409 }
410 }
411
412 #[must_use]
414 pub fn as_delete_marker(&self) -> Option<&S3DeleteMarker> {
415 match self {
416 Self::Object(_) => None,
417 Self::DeleteMarker(dm) => Some(dm),
418 }
419 }
420}
421
422#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn test_should_use_default_owner() {
432 let owner = Owner::default();
433 assert_eq!(owner.display_name, "webfile");
434 assert!(!owner.id.is_empty());
435 }
436
437 #[test]
438 fn test_should_display_owner() {
439 let owner = Owner {
440 id: "abc123".to_owned(),
441 display_name: "alice".to_owned(),
442 };
443 assert_eq!(format!("{owner}"), "alice(abc123)");
444 }
445
446 #[test]
447 fn test_should_default_canned_acl_to_private() {
448 assert_eq!(CannedAcl::default(), CannedAcl::Private);
449 assert_eq!(CannedAcl::default().as_str(), "private");
450 }
451
452 #[test]
453 fn test_should_roundtrip_canned_acl_from_str() {
454 let cases = [
455 ("private", CannedAcl::Private),
456 ("public-read", CannedAcl::PublicRead),
457 ("public-read-write", CannedAcl::PublicReadWrite),
458 ("authenticated-read", CannedAcl::AuthenticatedRead),
459 ("aws-exec-read", CannedAcl::AwsExecRead),
460 ("bucket-owner-read", CannedAcl::BucketOwnerRead),
461 (
462 "bucket-owner-full-control",
463 CannedAcl::BucketOwnerFullControl,
464 ),
465 ("log-delivery-write", CannedAcl::LogDeliveryWrite),
466 ];
467 for (s, expected) in cases {
468 let parsed: CannedAcl = s.parse().unwrap_or_else(|_| panic!("failed to parse {s}"));
469 assert_eq!(parsed, expected);
470 assert_eq!(parsed.as_str(), s);
471 }
472 }
473
474 #[test]
475 fn test_should_reject_unknown_canned_acl() {
476 let result = "unknown-acl".parse::<CannedAcl>();
477 assert!(result.is_err());
478 }
479
480 #[test]
481 fn test_should_identify_object_as_not_delete_marker() {
482 let obj = make_test_object("test-key");
483 assert!(!obj.is_delete_marker());
484 }
485
486 #[test]
487 fn test_should_identify_delete_marker() {
488 let dm = S3DeleteMarker {
489 key: "test-key".to_owned(),
490 version_id: "v1".to_owned(),
491 last_modified: Utc::now(),
492 owner: Owner::default(),
493 };
494 assert!(dm.is_delete_marker());
495 }
496
497 #[test]
498 fn test_should_access_object_version_fields() {
499 let obj = make_test_object("my-key");
500 let version = ObjectVersion::Object(Box::new(obj));
501
502 assert_eq!(version.key(), "my-key");
503 assert_eq!(version.version_id(), "null");
504 assert!(!version.is_delete_marker());
505 assert!(version.as_object().is_some());
506 assert!(version.as_delete_marker().is_none());
507 }
508
509 #[test]
510 fn test_should_access_delete_marker_version_fields() {
511 let dm = S3DeleteMarker {
512 key: "deleted-key".to_owned(),
513 version_id: "dm-v1".to_owned(),
514 last_modified: Utc::now(),
515 owner: Owner::default(),
516 };
517 let version = ObjectVersion::DeleteMarker(dm);
518
519 assert_eq!(version.key(), "deleted-key");
520 assert_eq!(version.version_id(), "dm-v1");
521 assert!(version.is_delete_marker());
522 assert!(version.as_object().is_none());
523 assert!(version.as_delete_marker().is_some());
524 }
525
526 #[test]
527 fn test_should_default_object_metadata() {
528 let meta = ObjectMetadata::default();
529 assert!(meta.content_type.is_none());
530 assert!(meta.user_metadata.is_empty());
531 assert!(meta.tagging.is_empty());
532 assert_eq!(meta.acl, CannedAcl::Private);
533 assert!(meta.object_lock_mode.is_none());
534 }
535
536 #[test]
537 fn test_should_display_permission() {
538 assert_eq!(format!("{}", Permission::FullControl), "FULL_CONTROL");
539 assert_eq!(format!("{}", Permission::Read), "READ");
540 assert_eq!(format!("{}", Permission::Write), "WRITE");
541 assert_eq!(format!("{}", Permission::ReadAcp), "READ_ACP");
542 assert_eq!(format!("{}", Permission::WriteAcp), "WRITE_ACP");
543 }
544
545 fn make_test_object(key: &str) -> S3Object {
548 S3Object {
549 key: key.to_owned(),
550 version_id: "null".to_owned(),
551 etag: "\"d41d8cd98f00b204e9800998ecf8427e\"".to_owned(),
552 size: 0,
553 last_modified: Utc::now(),
554 storage_class: "STANDARD".to_owned(),
555 metadata: ObjectMetadata::default(),
556 owner: Owner::default(),
557 checksum: None,
558 parts_count: None,
559 part_etags: Vec::new(),
560 }
561 }
562}