1use std::collections::HashMap;
7
8use async_trait::async_trait;
9use jiff::Timestamp;
10use serde::{Deserialize, Serialize};
11use tokio::io::{AsyncWrite, AsyncWriteExt};
12
13use crate::cors::CorsRule;
14use crate::encryption::{BucketEncryption, ObjectEncryptionRequest};
15use crate::error::{Error, Result};
16use crate::lifecycle::LifecycleRule;
17use crate::multipart_copy::{
18 MultipartCopyCancellation, MultipartCopyOptions, MultipartCopyProgress, MultipartCopyResult,
19};
20use crate::object_lock::{
21 BucketObjectLockConfiguration, LegalHoldStatus, ObjectLockOptions, ObjectRetention,
22};
23use crate::path::RemotePath;
24use crate::replication::{
25 ReplicationCheckResult, ReplicationConfiguration, ReplicationResyncStartOptions,
26 ReplicationResyncStartResult, ReplicationResyncStatus,
27};
28use crate::select::SelectOptions;
29use crate::transfer_options::{
30 ObjectTransferMetadata, ObjectWriteOptions, TransferCopyOptions, TransferReadOptions,
31};
32
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
35pub struct CreateBucketOptions {
36 pub region: Option<String>,
38 pub versioning_enabled: bool,
40 pub object_lock_enabled: bool,
42}
43
44impl CreateBucketOptions {
45 pub fn for_cli(
47 region: Option<String>,
48 versioning_enabled: bool,
49 object_lock_enabled: bool,
50 ) -> Result<Self> {
51 let options = Self {
52 region,
53 versioning_enabled: versioning_enabled || object_lock_enabled,
54 object_lock_enabled,
55 };
56 options.validate()?;
57 Ok(options)
58 }
59
60 pub fn validate(&self) -> Result<()> {
62 if self.object_lock_enabled && !self.versioning_enabled {
63 return Err(Error::InvalidPath(
64 "Bucket Object Lock requires versioning to be enabled".to_string(),
65 ));
66 }
67 if self
68 .region
69 .as_deref()
70 .is_some_and(|region| region.trim().is_empty())
71 {
72 return Err(Error::InvalidPath(
73 "Bucket region cannot be empty".to_string(),
74 ));
75 }
76 Ok(())
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ObjectVersion {
83 pub key: String,
85
86 pub version_id: String,
88
89 pub is_latest: bool,
91
92 pub is_delete_marker: bool,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub last_modified: Option<Timestamp>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub size_bytes: Option<i64>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub etag: Option<String>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ObjectVersionListResult {
111 pub items: Vec<ObjectVersion>,
113
114 pub truncated: bool,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub continuation_token: Option<String>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub version_id_marker: Option<String>,
124}
125
126#[derive(Debug, Clone, Default, PartialEq, Eq)]
128pub struct ObjectReadOptions {
129 pub version_id: Option<String>,
131}
132
133#[derive(Debug, Clone, Default, PartialEq, Eq)]
135pub struct CopyObjectOptions {
136 pub source_version_id: Option<String>,
138 pub source_etag: Option<String>,
140}
141
142impl CopyObjectOptions {
143 pub fn for_source_version(source_version_id: Option<String>) -> Result<Self> {
145 Self::for_source_identity(source_version_id, None)
146 }
147
148 pub fn for_source_identity(
150 source_version_id: Option<String>,
151 source_etag: Option<String>,
152 ) -> Result<Self> {
153 if source_version_id.as_deref().is_some_and(str::is_empty) {
154 return Err(Error::InvalidPath(
155 "Source version ID cannot be empty".to_string(),
156 ));
157 }
158 if source_etag.as_deref().is_some_and(str::is_empty) {
159 return Err(Error::InvalidPath(
160 "Source ETag cannot be empty".to_string(),
161 ));
162 }
163 Ok(Self {
164 source_version_id,
165 source_etag,
166 })
167 }
168}
169
170impl ObjectReadOptions {
171 pub fn for_version(version_id: Option<String>) -> Result<Self> {
173 if version_id.as_deref().is_some_and(str::is_empty) {
174 return Err(Error::InvalidPath("Version ID cannot be empty".to_string()));
175 }
176 Ok(Self { version_id })
177 }
178}
179
180#[derive(Debug, Clone, Default, PartialEq, Eq)]
182pub struct ListObjectVersionsOptions {
183 pub max_keys: Option<i32>,
185 pub key_marker: Option<String>,
187 pub version_id_marker: Option<String>,
189}
190
191#[derive(Debug, Clone, Default, PartialEq, Eq)]
193pub struct DeleteRequestOptions {
194 pub version_id: Option<String>,
196 pub bypass_governance: bool,
198 pub force_delete: bool,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct ObjectVersionIdentifier {
205 pub key: String,
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub version_id: Option<String>,
210 pub is_delete_marker: bool,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216pub struct DeletedObject {
217 pub key: String,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub version_id: Option<String>,
222 pub is_delete_marker: bool,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228pub struct DeleteObjectFailure {
229 pub key: String,
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub version_id: Option<String>,
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub code: Option<String>,
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub message: Option<String>,
240}
241
242#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
244pub struct DeleteObjectsResult {
245 pub deleted: Vec<DeletedObject>,
247 pub failures: Vec<DeleteObjectFailure>,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct ObjectInfo {
254 pub key: String,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub size_bytes: Option<i64>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub size_human: Option<String>,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub last_modified: Option<Timestamp>,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub etag: Option<String>,
272
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub storage_class: Option<String>,
276
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub content_type: Option<String>,
280
281 #[serde(skip_serializing_if = "Option::is_none")]
283 pub metadata: Option<HashMap<String, String>>,
284
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub version_id: Option<String>,
288
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub source_version_id: Option<String>,
292
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub is_delete_marker: Option<bool>,
296
297 pub is_dir: bool,
299}
300
301impl ObjectInfo {
302 pub fn file(key: impl Into<String>, size: i64) -> Self {
304 Self {
305 key: key.into(),
306 size_bytes: Some(size),
307 size_human: Some(humansize::format_size(size as u64, humansize::BINARY)),
308 last_modified: None,
309 etag: None,
310 storage_class: None,
311 content_type: None,
312 metadata: None,
313 version_id: None,
314 source_version_id: None,
315 is_delete_marker: None,
316 is_dir: false,
317 }
318 }
319
320 pub fn dir(key: impl Into<String>) -> Self {
322 Self {
323 key: key.into(),
324 size_bytes: None,
325 size_human: None,
326 last_modified: None,
327 etag: None,
328 storage_class: None,
329 content_type: None,
330 metadata: None,
331 version_id: None,
332 source_version_id: None,
333 is_delete_marker: None,
334 is_dir: true,
335 }
336 }
337
338 pub fn bucket(name: impl Into<String>) -> Self {
340 Self {
341 key: name.into(),
342 size_bytes: None,
343 size_human: None,
344 last_modified: None,
345 etag: None,
346 storage_class: None,
347 content_type: None,
348 metadata: None,
349 version_id: None,
350 source_version_id: None,
351 is_delete_marker: None,
352 is_dir: true,
353 }
354 }
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct ListResult {
360 pub items: Vec<ObjectInfo>,
362
363 pub truncated: bool,
365
366 #[serde(skip_serializing_if = "Option::is_none")]
368 pub continuation_token: Option<String>,
369}
370
371#[derive(Debug, Clone, Default)]
373pub struct ListOptions {
374 pub max_keys: Option<i32>,
376
377 pub delimiter: Option<String>,
379
380 pub prefix: Option<String>,
382
383 pub continuation_token: Option<String>,
385
386 pub recursive: bool,
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
392pub struct MultipartIdentity {
393 #[serde(skip_serializing_if = "Option::is_none")]
395 pub id: Option<String>,
396
397 #[serde(skip_serializing_if = "Option::is_none")]
399 pub display_name: Option<String>,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
408pub struct MultipartUpload {
409 pub bucket: String,
411
412 pub key: String,
414
415 pub upload_id: String,
417
418 pub initiated: Option<Timestamp>,
420
421 pub size_bytes: Option<i64>,
423
424 pub storage_class: Option<String>,
426
427 #[serde(skip_serializing_if = "Option::is_none")]
429 pub initiator: Option<MultipartIdentity>,
430
431 #[serde(skip_serializing_if = "Option::is_none")]
433 pub owner: Option<MultipartIdentity>,
434
435 #[serde(skip_serializing_if = "Option::is_none")]
437 pub checksum_algorithm: Option<String>,
438
439 #[serde(skip_serializing_if = "Option::is_none")]
441 pub checksum_type: Option<String>,
442}
443
444#[derive(Debug, Clone, Default, PartialEq, Eq)]
446pub struct MultipartUploadListOptions {
447 pub prefix: Option<String>,
449
450 pub delimiter: Option<String>,
452
453 pub key_marker: Option<String>,
455
456 pub upload_id_marker: Option<String>,
458
459 pub max_uploads: Option<i32>,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465pub struct MultipartUploadListResult {
466 pub uploads: Vec<MultipartUpload>,
468
469 #[serde(default, skip_serializing_if = "Vec::is_empty")]
471 pub common_prefixes: Vec<String>,
472
473 pub truncated: bool,
475
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub next_key_marker: Option<String>,
479
480 #[serde(skip_serializing_if = "Option::is_none")]
482 pub next_upload_id_marker: Option<String>,
483}
484
485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
487pub struct AbortMultipartUploadRequest {
488 pub bucket: String,
490
491 pub key: String,
493
494 pub upload_id: String,
496}
497
498#[derive(Debug, Clone, Default)]
500pub struct Capabilities {
501 pub versioning: bool,
503
504 pub object_lock: bool,
506
507 pub tagging: bool,
509
510 pub anonymous: bool,
512
513 pub select: bool,
518
519 pub notifications: bool,
521
522 pub lifecycle: bool,
524
525 pub replication: bool,
527
528 pub cors: bool,
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
534#[serde(rename_all = "lowercase")]
535pub enum NotificationTarget {
536 Queue,
538 Topic,
540 Lambda,
542}
543
544#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
546pub struct BucketNotification {
547 #[serde(skip_serializing_if = "Option::is_none")]
549 pub id: Option<String>,
550 pub target: NotificationTarget,
552 pub arn: String,
554 pub events: Vec<String>,
556 #[serde(skip_serializing_if = "Option::is_none")]
558 pub prefix: Option<String>,
559 #[serde(skip_serializing_if = "Option::is_none")]
561 pub suffix: Option<String>,
562}
563
564#[async_trait]
568pub trait ObjectStore: Send + Sync {
569 async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
571
572 async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
574
575 async fn list_multipart_uploads(
577 &self,
578 bucket: &str,
579 options: MultipartUploadListOptions,
580 ) -> Result<MultipartUploadListResult> {
581 let _ = (bucket, options);
582 Err(Error::UnsupportedFeature(
583 "Multipart upload listing is not supported by this object store".to_string(),
584 ))
585 }
586
587 async fn abort_multipart_upload(&self, request: &AbortMultipartUploadRequest) -> Result<()> {
589 let _ = request;
590 Err(Error::UnsupportedFeature(
591 "Multipart upload cleanup is not supported by this object store".to_string(),
592 ))
593 }
594
595 async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
597
598 async fn head_object_with_options(
600 &self,
601 path: &RemotePath,
602 options: &ObjectReadOptions,
603 ) -> Result<ObjectInfo> {
604 if options.version_id.is_some() {
605 return Err(Error::UnsupportedFeature(
606 "Exact-version metadata reads are not implemented by this object store".to_string(),
607 ));
608 }
609 self.head_object(path).await
610 }
611
612 async fn head_object_with_transfer_options(
616 &self,
617 path: &RemotePath,
618 options: &TransferReadOptions,
619 ) -> Result<ObjectInfo> {
620 let legacy = options.legacy_read_options()?;
621 self.head_object_with_options(path, &legacy).await
622 }
623
624 async fn head_object_transfer_metadata(
626 &self,
627 _path: &RemotePath,
628 options: &TransferReadOptions,
629 ) -> Result<ObjectTransferMetadata> {
630 options.validate()?;
631 Err(Error::UnsupportedFeature(
632 "Complete transfer metadata is not implemented by this object store".to_string(),
633 ))
634 }
635
636 async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
638
639 async fn create_bucket(&self, bucket: &str) -> Result<()>;
641
642 async fn create_bucket_with_options(
647 &self,
648 bucket: &str,
649 options: &CreateBucketOptions,
650 ) -> Result<()> {
651 options.validate()?;
652 if options != &CreateBucketOptions::default() {
653 return Err(Error::UnsupportedFeature(
654 "Bucket creation options are not implemented by this object store".to_string(),
655 ));
656 }
657 self.create_bucket(bucket).await
658 }
659
660 async fn get_bucket_location(&self, _bucket: &str) -> Result<Option<String>> {
664 Err(Error::UnsupportedFeature(
665 "Bucket location inspection is not implemented by this object store".to_string(),
666 ))
667 }
668
669 async fn delete_bucket(&self, bucket: &str) -> Result<()>;
671
672 async fn capabilities(&self) -> Result<Capabilities>;
674
675 async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
677
678 async fn get_object_with_options(
680 &self,
681 path: &RemotePath,
682 options: &ObjectReadOptions,
683 ) -> Result<Vec<u8>> {
684 if options.version_id.is_some() {
685 return Err(Error::UnsupportedFeature(
686 "Exact-version object reads are not implemented by this object store".to_string(),
687 ));
688 }
689 self.get_object(path).await
690 }
691
692 async fn get_object_with_transfer_options(
696 &self,
697 path: &RemotePath,
698 options: &TransferReadOptions,
699 ) -> Result<Vec<u8>> {
700 let legacy = options.legacy_read_options()?;
701 self.get_object_with_options(path, &legacy).await
702 }
703
704 async fn write_object_to_with_options(
706 &self,
707 path: &RemotePath,
708 options: &ObjectReadOptions,
709 writer: &mut (dyn AsyncWrite + Send + Unpin),
710 max_bytes: Option<u64>,
711 ) -> Result<u64> {
712 let data = self.get_object_with_options(path, options).await?;
713 let write_len = max_bytes
714 .and_then(|limit| usize::try_from(limit).ok())
715 .map(|limit| limit.min(data.len()))
716 .unwrap_or(data.len());
717 writer.write_all(&data[..write_len]).await?;
718 writer.flush().await?;
719 Ok(write_len as u64)
720 }
721
722 async fn write_object_to_with_transfer_options(
726 &self,
727 path: &RemotePath,
728 options: &TransferReadOptions,
729 writer: &mut (dyn AsyncWrite + Send + Unpin),
730 max_bytes: Option<u64>,
731 ) -> Result<u64> {
732 let legacy = options.legacy_read_options()?;
733 self.write_object_to_with_options(path, &legacy, writer, max_bytes)
734 .await
735 }
736
737 async fn put_object(
739 &self,
740 path: &RemotePath,
741 data: Vec<u8>,
742 content_type: Option<&str>,
743 encryption: Option<&ObjectEncryptionRequest>,
744 ) -> Result<ObjectInfo>;
745
746 async fn put_object_with_options(
751 &self,
752 path: &RemotePath,
753 data: Vec<u8>,
754 options: &ObjectWriteOptions,
755 ) -> Result<ObjectInfo> {
756 let (content_type, encryption) = options.legacy_put_arguments()?;
757 self.put_object(path, data, content_type, encryption).await
758 }
759
760 async fn delete_object(&self, path: &RemotePath) -> Result<()>;
762
763 async fn delete_object_with_options(
765 &self,
766 path: &RemotePath,
767 options: DeleteRequestOptions,
768 ) -> Result<DeletedObject> {
769 if options.version_id.is_some() || options.bypass_governance || options.force_delete {
770 return Err(Error::UnsupportedFeature(
771 "Version-aware or policy-bypassing deletion is not implemented by this object store"
772 .to_string(),
773 ));
774 }
775 self.delete_object(path).await?;
776 Ok(DeletedObject {
777 key: path.key.clone(),
778 version_id: None,
779 is_delete_marker: false,
780 })
781 }
782
783 async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
785
786 async fn delete_object_versions(
788 &self,
789 _bucket: &str,
790 _objects: Vec<ObjectVersionIdentifier>,
791 _options: DeleteRequestOptions,
792 ) -> Result<DeleteObjectsResult> {
793 Err(Error::UnsupportedFeature(
794 "Multi-object version deletion is not implemented by this object store".to_string(),
795 ))
796 }
797
798 async fn copy_object(
800 &self,
801 src: &RemotePath,
802 dst: &RemotePath,
803 encryption: Option<&ObjectEncryptionRequest>,
804 ) -> Result<ObjectInfo>;
805
806 async fn copy_object_with_options(
808 &self,
809 src: &RemotePath,
810 dst: &RemotePath,
811 options: &CopyObjectOptions,
812 encryption: Option<&ObjectEncryptionRequest>,
813 ) -> Result<ObjectInfo> {
814 if options.source_version_id.is_some() || options.source_etag.is_some() {
815 return Err(Error::UnsupportedFeature(
816 "Exact-source server-side copy is not implemented by this object store".to_string(),
817 ));
818 }
819 self.copy_object(src, dst, encryption).await
820 }
821
822 async fn copy_object_with_transfer_options(
827 &self,
828 src: &RemotePath,
829 dst: &RemotePath,
830 options: &TransferCopyOptions,
831 ) -> Result<ObjectInfo> {
832 let (legacy, encryption) = options.legacy_copy_arguments()?;
833 self.copy_object_with_options(src, dst, &legacy, encryption)
834 .await
835 }
836
837 async fn multipart_copy(
842 &self,
843 _src: &RemotePath,
844 _dst: &RemotePath,
845 _options: &MultipartCopyOptions,
846 _cancellation: &MultipartCopyCancellation,
847 _encryption: Option<&ObjectEncryptionRequest>,
848 _on_progress: &MultipartCopyProgress<'_>,
849 ) -> Result<MultipartCopyResult> {
850 Err(Error::UnsupportedFeature(
851 "Multipart server-side copy is not implemented by this object store".to_string(),
852 ))
853 }
854
855 async fn multipart_copy_with_transfer_options(
860 &self,
861 src: &RemotePath,
862 dst: &RemotePath,
863 multipart: &MultipartCopyOptions,
864 transfer: &TransferCopyOptions,
865 cancellation: &MultipartCopyCancellation,
866 on_progress: &MultipartCopyProgress<'_>,
867 ) -> Result<MultipartCopyResult> {
868 transfer.validate_multipart_source_version(multipart.source_version_id.as_deref())?;
869 let (_, encryption) = transfer.legacy_copy_arguments()?;
870 self.multipart_copy(src, dst, multipart, cancellation, encryption, on_progress)
871 .await
872 }
873
874 async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
876
877 async fn presign_put(
879 &self,
880 path: &RemotePath,
881 expires_secs: u64,
882 content_type: Option<&str>,
883 ) -> Result<String>;
884
885 async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
889
890 async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
892
893 async fn get_bucket_object_lock_configuration(
897 &self,
898 _bucket: &str,
899 ) -> Result<Option<BucketObjectLockConfiguration>> {
900 Err(Error::UnsupportedFeature(
901 "Bucket Object Lock configuration is not implemented by this object store".to_string(),
902 ))
903 }
904
905 async fn put_bucket_object_lock_configuration(
907 &self,
908 _bucket: &str,
909 _configuration: BucketObjectLockConfiguration,
910 ) -> Result<()> {
911 Err(Error::UnsupportedFeature(
912 "Bucket Object Lock configuration is not implemented by this object store".to_string(),
913 ))
914 }
915
916 async fn get_object_retention(
918 &self,
919 _path: &RemotePath,
920 _options: &ObjectLockOptions,
921 ) -> Result<Option<ObjectRetention>> {
922 Err(Error::UnsupportedFeature(
923 "Object retention is not implemented by this object store".to_string(),
924 ))
925 }
926
927 async fn put_object_retention(
929 &self,
930 _path: &RemotePath,
931 _retention: Option<ObjectRetention>,
932 _options: &ObjectLockOptions,
933 ) -> Result<()> {
934 Err(Error::UnsupportedFeature(
935 "Object retention is not implemented by this object store".to_string(),
936 ))
937 }
938
939 async fn get_object_legal_hold(
941 &self,
942 _path: &RemotePath,
943 _options: &ObjectLockOptions,
944 ) -> Result<LegalHoldStatus> {
945 Err(Error::UnsupportedFeature(
946 "Object legal hold is not implemented by this object store".to_string(),
947 ))
948 }
949
950 async fn put_object_legal_hold(
952 &self,
953 _path: &RemotePath,
954 _status: LegalHoldStatus,
955 _options: &ObjectLockOptions,
956 ) -> Result<()> {
957 Err(Error::UnsupportedFeature(
958 "Object legal hold is not implemented by this object store".to_string(),
959 ))
960 }
961
962 async fn get_bucket_encryption(&self, bucket: &str) -> Result<Option<BucketEncryption>>;
964
965 async fn set_bucket_encryption(&self, bucket: &str, encryption: BucketEncryption)
967 -> Result<()>;
968
969 async fn delete_bucket_encryption(&self, bucket: &str) -> Result<()>;
971
972 async fn list_object_versions(
974 &self,
975 path: &RemotePath,
976 max_keys: Option<i32>,
977 ) -> Result<Vec<ObjectVersion>>;
978
979 async fn list_object_versions_page_with_options(
981 &self,
982 _path: &RemotePath,
983 _options: &ListObjectVersionsOptions,
984 ) -> Result<ObjectVersionListResult> {
985 Err(Error::UnsupportedFeature(
986 "Paginated object version listing is not implemented by this object store".to_string(),
987 ))
988 }
989
990 async fn get_object_tags(
992 &self,
993 path: &RemotePath,
994 ) -> Result<std::collections::HashMap<String, String>>;
995
996 async fn get_bucket_tags(
998 &self,
999 bucket: &str,
1000 ) -> Result<std::collections::HashMap<String, String>>;
1001
1002 async fn set_object_tags(
1004 &self,
1005 path: &RemotePath,
1006 tags: std::collections::HashMap<String, String>,
1007 ) -> Result<()>;
1008
1009 async fn set_bucket_tags(
1011 &self,
1012 bucket: &str,
1013 tags: std::collections::HashMap<String, String>,
1014 ) -> Result<()>;
1015
1016 async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
1018
1019 async fn delete_bucket_tags(&self, bucket: &str) -> Result<()>;
1021
1022 async fn get_bucket_policy(&self, bucket: &str) -> Result<Option<String>>;
1024
1025 async fn set_bucket_policy(&self, bucket: &str, policy: &str) -> Result<()>;
1027
1028 async fn delete_bucket_policy(&self, bucket: &str) -> Result<()>;
1030
1031 async fn get_bucket_notifications(&self, bucket: &str) -> Result<Vec<BucketNotification>>;
1033
1034 async fn set_bucket_notifications(
1036 &self,
1037 bucket: &str,
1038 notifications: Vec<BucketNotification>,
1039 ) -> Result<()>;
1040
1041 async fn get_bucket_lifecycle(&self, bucket: &str) -> Result<Vec<LifecycleRule>>;
1045
1046 async fn set_bucket_lifecycle(&self, bucket: &str, rules: Vec<LifecycleRule>) -> Result<()>;
1048
1049 async fn delete_bucket_lifecycle(&self, bucket: &str) -> Result<()>;
1051
1052 async fn restore_object(&self, path: &RemotePath, days: i32) -> Result<()>;
1054
1055 async fn get_bucket_replication(
1059 &self,
1060 bucket: &str,
1061 ) -> Result<Option<ReplicationConfiguration>>;
1062
1063 async fn set_bucket_replication(
1065 &self,
1066 bucket: &str,
1067 config: ReplicationConfiguration,
1068 ) -> Result<()>;
1069
1070 async fn delete_bucket_replication(&self, bucket: &str) -> Result<()>;
1072
1073 async fn check_bucket_replication(&self, bucket: &str) -> Result<()>;
1076
1077 async fn check_bucket_replication_detailed(
1080 &self,
1081 bucket: &str,
1082 ) -> Result<ReplicationCheckResult> {
1083 self.check_bucket_replication(bucket).await?;
1084 Ok(ReplicationCheckResult::legacy_success())
1085 }
1086
1087 async fn start_bucket_replication_resync(
1089 &self,
1090 bucket: &str,
1091 options: ReplicationResyncStartOptions,
1092 ) -> Result<ReplicationResyncStartResult>;
1093
1094 async fn bucket_replication_resync_status(
1096 &self,
1097 bucket: &str,
1098 target_arn: Option<&str>,
1099 ) -> Result<ReplicationResyncStatus>;
1100
1101 async fn get_bucket_cors(&self, bucket: &str) -> Result<Vec<CorsRule>>;
1103
1104 async fn set_bucket_cors(&self, bucket: &str, rules: Vec<CorsRule>) -> Result<()>;
1106
1107 async fn delete_bucket_cors(&self, bucket: &str) -> Result<()>;
1109
1110 async fn select_object_content(
1112 &self,
1113 path: &RemotePath,
1114 options: &SelectOptions,
1115 writer: &mut (dyn AsyncWrite + Send + Unpin),
1116 ) -> Result<()>;
1117 }
1122
1123#[cfg(test)]
1124mod tests {
1125 use super::*;
1126
1127 #[test]
1128 fn create_bucket_options_reject_lock_without_versioning() {
1129 let options = CreateBucketOptions {
1130 region: Some("us-east-1".to_string()),
1131 versioning_enabled: false,
1132 object_lock_enabled: true,
1133 };
1134
1135 let error = options
1136 .validate()
1137 .expect_err("Object Lock without versioning must be rejected");
1138
1139 assert!(matches!(error, crate::Error::InvalidPath(_)));
1140 }
1141
1142 #[test]
1143 fn create_bucket_options_normalize_cli_lock_to_versioning() {
1144 let options = CreateBucketOptions::for_cli(Some("us-east-1".to_string()), false, true)
1145 .expect("CLI Object Lock options should be valid");
1146
1147 assert!(options.object_lock_enabled);
1148 assert!(options.versioning_enabled);
1149 assert_eq!(options.region.as_deref(), Some("us-east-1"));
1150 }
1151
1152 #[test]
1153 fn test_object_info_file() {
1154 let info = ObjectInfo::file("test.txt", 1024);
1155 assert_eq!(info.key, "test.txt");
1156 assert_eq!(info.size_bytes, Some(1024));
1157 assert!(!info.is_dir);
1158 assert_eq!(info.version_id, None);
1159 assert_eq!(info.source_version_id, None);
1160 assert_eq!(info.is_delete_marker, None);
1161 }
1162
1163 #[test]
1164 fn object_read_options_reject_empty_version_ids() {
1165 let error = ObjectReadOptions::for_version(Some(String::new()))
1166 .expect_err("empty version IDs must be rejected");
1167
1168 assert!(matches!(error, crate::Error::InvalidPath(_)));
1169 }
1170
1171 #[test]
1172 fn versioned_delete_targets_are_serializable_for_structured_output() {
1173 let target = ObjectVersionIdentifier {
1174 key: "reports/a.csv".to_string(),
1175 version_id: Some("v1".to_string()),
1176 is_delete_marker: true,
1177 };
1178
1179 let json = serde_json::to_value(target).expect("serialize versioned delete target");
1180 assert_eq!(json["key"], "reports/a.csv");
1181 assert_eq!(json["version_id"], "v1");
1182 assert_eq!(json["is_delete_marker"], true);
1183 }
1184
1185 #[test]
1186 fn object_info_version_fields_are_optional_and_serializable() {
1187 let current = serde_json::to_value(ObjectInfo::file("current.txt", 1))
1188 .expect("serialize current object info");
1189 assert!(current.get("version_id").is_none());
1190 assert!(current.get("source_version_id").is_none());
1191 assert!(current.get("is_delete_marker").is_none());
1192
1193 let mut copied = ObjectInfo::file("copy.txt", 1);
1194 copied.version_id = Some("destination-v2".to_string());
1195 copied.source_version_id = Some("source-v1".to_string());
1196 let copied = serde_json::to_value(copied).expect("serialize copy object info");
1197 assert_eq!(copied["version_id"], "destination-v2");
1198 assert_eq!(copied["source_version_id"], "source-v1");
1199 }
1200
1201 #[test]
1202 fn test_object_info_dir() {
1203 let info = ObjectInfo::dir("path/to/dir/");
1204 assert_eq!(info.key, "path/to/dir/");
1205 assert!(info.is_dir);
1206 assert!(info.size_bytes.is_none());
1207 }
1208
1209 #[test]
1210 fn test_object_info_bucket() {
1211 let info = ObjectInfo::bucket("my-bucket");
1212 assert_eq!(info.key, "my-bucket");
1213 assert!(info.is_dir);
1214 }
1215
1216 #[test]
1217 fn test_object_info_metadata_default_none() {
1218 let info = ObjectInfo::file("test.txt", 1024);
1219 assert!(info.metadata.is_none());
1220 }
1221
1222 #[test]
1223 fn test_object_info_metadata_set() {
1224 let mut info = ObjectInfo::file("test.txt", 1024);
1225 let mut meta = HashMap::new();
1226 meta.insert("content-disposition".to_string(), "attachment".to_string());
1227 meta.insert("custom-key".to_string(), "custom-value".to_string());
1228 info.metadata = Some(meta);
1229
1230 let metadata = info.metadata.as_ref().expect("metadata should be Some");
1231 assert_eq!(metadata.len(), 2);
1232 assert_eq!(metadata.get("content-disposition").unwrap(), "attachment");
1233 assert_eq!(metadata.get("custom-key").unwrap(), "custom-value");
1234 }
1235
1236 #[test]
1237 fn multipart_upload_serializes_stable_identity_and_server_metadata() {
1238 let upload = MultipartUpload {
1239 bucket: "archive".to_string(),
1240 key: "backups/data.tar".to_string(),
1241 upload_id: "upload-123".to_string(),
1242 initiated: Some(
1243 "2026-07-21T04:00:00Z"
1244 .parse()
1245 .expect("test timestamp should be valid"),
1246 ),
1247 size_bytes: None,
1248 storage_class: Some("STANDARD".to_string()),
1249 initiator: Some(MultipartIdentity {
1250 id: Some("user-1".to_string()),
1251 display_name: Some("backup-agent".to_string()),
1252 }),
1253 owner: None,
1254 checksum_algorithm: Some("CRC64NVME".to_string()),
1255 checksum_type: Some("FULL_OBJECT".to_string()),
1256 };
1257
1258 let value = serde_json::to_value(upload).expect("multipart upload should serialize");
1259 assert_eq!(value["bucket"], "archive");
1260 assert_eq!(value["key"], "backups/data.tar");
1261 assert_eq!(value["upload_id"], "upload-123");
1262 assert_eq!(value["initiated"], "2026-07-21T04:00:00Z");
1263 assert!(value["size_bytes"].is_null());
1264 assert_eq!(value["storage_class"], "STANDARD");
1265 assert_eq!(value["initiator"]["id"], "user-1");
1266 }
1267
1268 #[test]
1269 fn multipart_list_options_default_to_the_first_unfiltered_page() {
1270 let options = MultipartUploadListOptions::default();
1271
1272 assert!(options.prefix.is_none());
1273 assert!(options.delimiter.is_none());
1274 assert!(options.key_marker.is_none());
1275 assert!(options.upload_id_marker.is_none());
1276 assert!(options.max_uploads.is_none());
1277 }
1278}