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}
139
140impl CopyObjectOptions {
141 pub fn for_source_version(source_version_id: Option<String>) -> Result<Self> {
143 if source_version_id.as_deref().is_some_and(str::is_empty) {
144 return Err(Error::InvalidPath(
145 "Source version ID cannot be empty".to_string(),
146 ));
147 }
148 Ok(Self { source_version_id })
149 }
150}
151
152impl ObjectReadOptions {
153 pub fn for_version(version_id: Option<String>) -> Result<Self> {
155 if version_id.as_deref().is_some_and(str::is_empty) {
156 return Err(Error::InvalidPath("Version ID cannot be empty".to_string()));
157 }
158 Ok(Self { version_id })
159 }
160}
161
162#[derive(Debug, Clone, Default, PartialEq, Eq)]
164pub struct ListObjectVersionsOptions {
165 pub max_keys: Option<i32>,
167 pub key_marker: Option<String>,
169 pub version_id_marker: Option<String>,
171}
172
173#[derive(Debug, Clone, Default, PartialEq, Eq)]
175pub struct DeleteRequestOptions {
176 pub version_id: Option<String>,
178 pub bypass_governance: bool,
180 pub force_delete: bool,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct ObjectVersionIdentifier {
187 pub key: String,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub version_id: Option<String>,
192 pub is_delete_marker: bool,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
198pub struct DeletedObject {
199 pub key: String,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub version_id: Option<String>,
204 pub is_delete_marker: bool,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210pub struct DeleteObjectFailure {
211 pub key: String,
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub version_id: Option<String>,
216 #[serde(skip_serializing_if = "Option::is_none")]
218 pub code: Option<String>,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub message: Option<String>,
222}
223
224#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
226pub struct DeleteObjectsResult {
227 pub deleted: Vec<DeletedObject>,
229 pub failures: Vec<DeleteObjectFailure>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct ObjectInfo {
236 pub key: String,
238
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub size_bytes: Option<i64>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub size_human: Option<String>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub last_modified: Option<Timestamp>,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub etag: Option<String>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub storage_class: Option<String>,
258
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub content_type: Option<String>,
262
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub metadata: Option<HashMap<String, String>>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub version_id: Option<String>,
270
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub source_version_id: Option<String>,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub is_delete_marker: Option<bool>,
278
279 pub is_dir: bool,
281}
282
283impl ObjectInfo {
284 pub fn file(key: impl Into<String>, size: i64) -> Self {
286 Self {
287 key: key.into(),
288 size_bytes: Some(size),
289 size_human: Some(humansize::format_size(size as u64, humansize::BINARY)),
290 last_modified: None,
291 etag: None,
292 storage_class: None,
293 content_type: None,
294 metadata: None,
295 version_id: None,
296 source_version_id: None,
297 is_delete_marker: None,
298 is_dir: false,
299 }
300 }
301
302 pub fn dir(key: impl Into<String>) -> Self {
304 Self {
305 key: key.into(),
306 size_bytes: None,
307 size_human: None,
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: true,
317 }
318 }
319
320 pub fn bucket(name: impl Into<String>) -> Self {
322 Self {
323 key: name.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
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ListResult {
342 pub items: Vec<ObjectInfo>,
344
345 pub truncated: bool,
347
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub continuation_token: Option<String>,
351}
352
353#[derive(Debug, Clone, Default)]
355pub struct ListOptions {
356 pub max_keys: Option<i32>,
358
359 pub delimiter: Option<String>,
361
362 pub prefix: Option<String>,
364
365 pub continuation_token: Option<String>,
367
368 pub recursive: bool,
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
374pub struct MultipartIdentity {
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub id: Option<String>,
378
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub display_name: Option<String>,
382}
383
384#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
390pub struct MultipartUpload {
391 pub bucket: String,
393
394 pub key: String,
396
397 pub upload_id: String,
399
400 pub initiated: Option<Timestamp>,
402
403 pub size_bytes: Option<i64>,
405
406 pub storage_class: Option<String>,
408
409 #[serde(skip_serializing_if = "Option::is_none")]
411 pub initiator: Option<MultipartIdentity>,
412
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub owner: Option<MultipartIdentity>,
416
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub checksum_algorithm: Option<String>,
420
421 #[serde(skip_serializing_if = "Option::is_none")]
423 pub checksum_type: Option<String>,
424}
425
426#[derive(Debug, Clone, Default, PartialEq, Eq)]
428pub struct MultipartUploadListOptions {
429 pub prefix: Option<String>,
431
432 pub delimiter: Option<String>,
434
435 pub key_marker: Option<String>,
437
438 pub upload_id_marker: Option<String>,
440
441 pub max_uploads: Option<i32>,
443}
444
445#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447pub struct MultipartUploadListResult {
448 pub uploads: Vec<MultipartUpload>,
450
451 #[serde(default, skip_serializing_if = "Vec::is_empty")]
453 pub common_prefixes: Vec<String>,
454
455 pub truncated: bool,
457
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub next_key_marker: Option<String>,
461
462 #[serde(skip_serializing_if = "Option::is_none")]
464 pub next_upload_id_marker: Option<String>,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct AbortMultipartUploadRequest {
470 pub bucket: String,
472
473 pub key: String,
475
476 pub upload_id: String,
478}
479
480#[derive(Debug, Clone, Default)]
482pub struct Capabilities {
483 pub versioning: bool,
485
486 pub object_lock: bool,
488
489 pub tagging: bool,
491
492 pub anonymous: bool,
494
495 pub select: bool,
500
501 pub notifications: bool,
503
504 pub lifecycle: bool,
506
507 pub replication: bool,
509
510 pub cors: bool,
512}
513
514#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
516#[serde(rename_all = "lowercase")]
517pub enum NotificationTarget {
518 Queue,
520 Topic,
522 Lambda,
524}
525
526#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
528pub struct BucketNotification {
529 #[serde(skip_serializing_if = "Option::is_none")]
531 pub id: Option<String>,
532 pub target: NotificationTarget,
534 pub arn: String,
536 pub events: Vec<String>,
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub prefix: Option<String>,
541 #[serde(skip_serializing_if = "Option::is_none")]
543 pub suffix: Option<String>,
544}
545
546#[async_trait]
550pub trait ObjectStore: Send + Sync {
551 async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
553
554 async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
556
557 async fn list_multipart_uploads(
559 &self,
560 bucket: &str,
561 options: MultipartUploadListOptions,
562 ) -> Result<MultipartUploadListResult> {
563 let _ = (bucket, options);
564 Err(Error::UnsupportedFeature(
565 "Multipart upload listing is not supported by this object store".to_string(),
566 ))
567 }
568
569 async fn abort_multipart_upload(&self, request: &AbortMultipartUploadRequest) -> Result<()> {
571 let _ = request;
572 Err(Error::UnsupportedFeature(
573 "Multipart upload cleanup is not supported by this object store".to_string(),
574 ))
575 }
576
577 async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
579
580 async fn head_object_with_options(
582 &self,
583 path: &RemotePath,
584 options: &ObjectReadOptions,
585 ) -> Result<ObjectInfo> {
586 if options.version_id.is_some() {
587 return Err(Error::UnsupportedFeature(
588 "Exact-version metadata reads are not implemented by this object store".to_string(),
589 ));
590 }
591 self.head_object(path).await
592 }
593
594 async fn head_object_with_transfer_options(
598 &self,
599 path: &RemotePath,
600 options: &TransferReadOptions,
601 ) -> Result<ObjectInfo> {
602 let legacy = options.legacy_read_options()?;
603 self.head_object_with_options(path, &legacy).await
604 }
605
606 async fn head_object_transfer_metadata(
608 &self,
609 _path: &RemotePath,
610 options: &TransferReadOptions,
611 ) -> Result<ObjectTransferMetadata> {
612 options.validate()?;
613 Err(Error::UnsupportedFeature(
614 "Complete transfer metadata is not implemented by this object store".to_string(),
615 ))
616 }
617
618 async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
620
621 async fn create_bucket(&self, bucket: &str) -> Result<()>;
623
624 async fn create_bucket_with_options(
629 &self,
630 bucket: &str,
631 options: &CreateBucketOptions,
632 ) -> Result<()> {
633 options.validate()?;
634 if options != &CreateBucketOptions::default() {
635 return Err(Error::UnsupportedFeature(
636 "Bucket creation options are not implemented by this object store".to_string(),
637 ));
638 }
639 self.create_bucket(bucket).await
640 }
641
642 async fn get_bucket_location(&self, _bucket: &str) -> Result<Option<String>> {
646 Err(Error::UnsupportedFeature(
647 "Bucket location inspection is not implemented by this object store".to_string(),
648 ))
649 }
650
651 async fn delete_bucket(&self, bucket: &str) -> Result<()>;
653
654 async fn capabilities(&self) -> Result<Capabilities>;
656
657 async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
659
660 async fn get_object_with_options(
662 &self,
663 path: &RemotePath,
664 options: &ObjectReadOptions,
665 ) -> Result<Vec<u8>> {
666 if options.version_id.is_some() {
667 return Err(Error::UnsupportedFeature(
668 "Exact-version object reads are not implemented by this object store".to_string(),
669 ));
670 }
671 self.get_object(path).await
672 }
673
674 async fn get_object_with_transfer_options(
678 &self,
679 path: &RemotePath,
680 options: &TransferReadOptions,
681 ) -> Result<Vec<u8>> {
682 let legacy = options.legacy_read_options()?;
683 self.get_object_with_options(path, &legacy).await
684 }
685
686 async fn write_object_to_with_options(
688 &self,
689 path: &RemotePath,
690 options: &ObjectReadOptions,
691 writer: &mut (dyn AsyncWrite + Send + Unpin),
692 max_bytes: Option<u64>,
693 ) -> Result<u64> {
694 let data = self.get_object_with_options(path, options).await?;
695 let write_len = max_bytes
696 .and_then(|limit| usize::try_from(limit).ok())
697 .map(|limit| limit.min(data.len()))
698 .unwrap_or(data.len());
699 writer.write_all(&data[..write_len]).await?;
700 writer.flush().await?;
701 Ok(write_len as u64)
702 }
703
704 async fn write_object_to_with_transfer_options(
708 &self,
709 path: &RemotePath,
710 options: &TransferReadOptions,
711 writer: &mut (dyn AsyncWrite + Send + Unpin),
712 max_bytes: Option<u64>,
713 ) -> Result<u64> {
714 let legacy = options.legacy_read_options()?;
715 self.write_object_to_with_options(path, &legacy, writer, max_bytes)
716 .await
717 }
718
719 async fn put_object(
721 &self,
722 path: &RemotePath,
723 data: Vec<u8>,
724 content_type: Option<&str>,
725 encryption: Option<&ObjectEncryptionRequest>,
726 ) -> Result<ObjectInfo>;
727
728 async fn put_object_with_options(
733 &self,
734 path: &RemotePath,
735 data: Vec<u8>,
736 options: &ObjectWriteOptions,
737 ) -> Result<ObjectInfo> {
738 let (content_type, encryption) = options.legacy_put_arguments()?;
739 self.put_object(path, data, content_type, encryption).await
740 }
741
742 async fn delete_object(&self, path: &RemotePath) -> Result<()>;
744
745 async fn delete_object_with_options(
747 &self,
748 path: &RemotePath,
749 options: DeleteRequestOptions,
750 ) -> Result<DeletedObject> {
751 if options.version_id.is_some() || options.bypass_governance || options.force_delete {
752 return Err(Error::UnsupportedFeature(
753 "Version-aware or policy-bypassing deletion is not implemented by this object store"
754 .to_string(),
755 ));
756 }
757 self.delete_object(path).await?;
758 Ok(DeletedObject {
759 key: path.key.clone(),
760 version_id: None,
761 is_delete_marker: false,
762 })
763 }
764
765 async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
767
768 async fn delete_object_versions(
770 &self,
771 _bucket: &str,
772 _objects: Vec<ObjectVersionIdentifier>,
773 _options: DeleteRequestOptions,
774 ) -> Result<DeleteObjectsResult> {
775 Err(Error::UnsupportedFeature(
776 "Multi-object version deletion is not implemented by this object store".to_string(),
777 ))
778 }
779
780 async fn copy_object(
782 &self,
783 src: &RemotePath,
784 dst: &RemotePath,
785 encryption: Option<&ObjectEncryptionRequest>,
786 ) -> Result<ObjectInfo>;
787
788 async fn copy_object_with_options(
790 &self,
791 src: &RemotePath,
792 dst: &RemotePath,
793 options: &CopyObjectOptions,
794 encryption: Option<&ObjectEncryptionRequest>,
795 ) -> Result<ObjectInfo> {
796 if options.source_version_id.is_some() {
797 return Err(Error::UnsupportedFeature(
798 "Exact-version server-side copy is not implemented by this object store"
799 .to_string(),
800 ));
801 }
802 self.copy_object(src, dst, encryption).await
803 }
804
805 async fn copy_object_with_transfer_options(
810 &self,
811 src: &RemotePath,
812 dst: &RemotePath,
813 options: &TransferCopyOptions,
814 ) -> Result<ObjectInfo> {
815 let (legacy, encryption) = options.legacy_copy_arguments()?;
816 self.copy_object_with_options(src, dst, &legacy, encryption)
817 .await
818 }
819
820 async fn multipart_copy(
825 &self,
826 _src: &RemotePath,
827 _dst: &RemotePath,
828 _options: &MultipartCopyOptions,
829 _cancellation: &MultipartCopyCancellation,
830 _encryption: Option<&ObjectEncryptionRequest>,
831 _on_progress: &MultipartCopyProgress<'_>,
832 ) -> Result<MultipartCopyResult> {
833 Err(Error::UnsupportedFeature(
834 "Multipart server-side copy is not implemented by this object store".to_string(),
835 ))
836 }
837
838 async fn multipart_copy_with_transfer_options(
843 &self,
844 src: &RemotePath,
845 dst: &RemotePath,
846 multipart: &MultipartCopyOptions,
847 transfer: &TransferCopyOptions,
848 cancellation: &MultipartCopyCancellation,
849 on_progress: &MultipartCopyProgress<'_>,
850 ) -> Result<MultipartCopyResult> {
851 transfer.validate_multipart_source_version(multipart.source_version_id.as_deref())?;
852 let (_, encryption) = transfer.legacy_copy_arguments()?;
853 self.multipart_copy(src, dst, multipart, cancellation, encryption, on_progress)
854 .await
855 }
856
857 async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
859
860 async fn presign_put(
862 &self,
863 path: &RemotePath,
864 expires_secs: u64,
865 content_type: Option<&str>,
866 ) -> Result<String>;
867
868 async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
872
873 async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
875
876 async fn get_bucket_object_lock_configuration(
880 &self,
881 _bucket: &str,
882 ) -> Result<Option<BucketObjectLockConfiguration>> {
883 Err(Error::UnsupportedFeature(
884 "Bucket Object Lock configuration is not implemented by this object store".to_string(),
885 ))
886 }
887
888 async fn put_bucket_object_lock_configuration(
890 &self,
891 _bucket: &str,
892 _configuration: BucketObjectLockConfiguration,
893 ) -> Result<()> {
894 Err(Error::UnsupportedFeature(
895 "Bucket Object Lock configuration is not implemented by this object store".to_string(),
896 ))
897 }
898
899 async fn get_object_retention(
901 &self,
902 _path: &RemotePath,
903 _options: &ObjectLockOptions,
904 ) -> Result<Option<ObjectRetention>> {
905 Err(Error::UnsupportedFeature(
906 "Object retention is not implemented by this object store".to_string(),
907 ))
908 }
909
910 async fn put_object_retention(
912 &self,
913 _path: &RemotePath,
914 _retention: Option<ObjectRetention>,
915 _options: &ObjectLockOptions,
916 ) -> Result<()> {
917 Err(Error::UnsupportedFeature(
918 "Object retention is not implemented by this object store".to_string(),
919 ))
920 }
921
922 async fn get_object_legal_hold(
924 &self,
925 _path: &RemotePath,
926 _options: &ObjectLockOptions,
927 ) -> Result<LegalHoldStatus> {
928 Err(Error::UnsupportedFeature(
929 "Object legal hold is not implemented by this object store".to_string(),
930 ))
931 }
932
933 async fn put_object_legal_hold(
935 &self,
936 _path: &RemotePath,
937 _status: LegalHoldStatus,
938 _options: &ObjectLockOptions,
939 ) -> Result<()> {
940 Err(Error::UnsupportedFeature(
941 "Object legal hold is not implemented by this object store".to_string(),
942 ))
943 }
944
945 async fn get_bucket_encryption(&self, bucket: &str) -> Result<Option<BucketEncryption>>;
947
948 async fn set_bucket_encryption(&self, bucket: &str, encryption: BucketEncryption)
950 -> Result<()>;
951
952 async fn delete_bucket_encryption(&self, bucket: &str) -> Result<()>;
954
955 async fn list_object_versions(
957 &self,
958 path: &RemotePath,
959 max_keys: Option<i32>,
960 ) -> Result<Vec<ObjectVersion>>;
961
962 async fn list_object_versions_page_with_options(
964 &self,
965 _path: &RemotePath,
966 _options: &ListObjectVersionsOptions,
967 ) -> Result<ObjectVersionListResult> {
968 Err(Error::UnsupportedFeature(
969 "Paginated object version listing is not implemented by this object store".to_string(),
970 ))
971 }
972
973 async fn get_object_tags(
975 &self,
976 path: &RemotePath,
977 ) -> Result<std::collections::HashMap<String, String>>;
978
979 async fn get_bucket_tags(
981 &self,
982 bucket: &str,
983 ) -> Result<std::collections::HashMap<String, String>>;
984
985 async fn set_object_tags(
987 &self,
988 path: &RemotePath,
989 tags: std::collections::HashMap<String, String>,
990 ) -> Result<()>;
991
992 async fn set_bucket_tags(
994 &self,
995 bucket: &str,
996 tags: std::collections::HashMap<String, String>,
997 ) -> Result<()>;
998
999 async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
1001
1002 async fn delete_bucket_tags(&self, bucket: &str) -> Result<()>;
1004
1005 async fn get_bucket_policy(&self, bucket: &str) -> Result<Option<String>>;
1007
1008 async fn set_bucket_policy(&self, bucket: &str, policy: &str) -> Result<()>;
1010
1011 async fn delete_bucket_policy(&self, bucket: &str) -> Result<()>;
1013
1014 async fn get_bucket_notifications(&self, bucket: &str) -> Result<Vec<BucketNotification>>;
1016
1017 async fn set_bucket_notifications(
1019 &self,
1020 bucket: &str,
1021 notifications: Vec<BucketNotification>,
1022 ) -> Result<()>;
1023
1024 async fn get_bucket_lifecycle(&self, bucket: &str) -> Result<Vec<LifecycleRule>>;
1028
1029 async fn set_bucket_lifecycle(&self, bucket: &str, rules: Vec<LifecycleRule>) -> Result<()>;
1031
1032 async fn delete_bucket_lifecycle(&self, bucket: &str) -> Result<()>;
1034
1035 async fn restore_object(&self, path: &RemotePath, days: i32) -> Result<()>;
1037
1038 async fn get_bucket_replication(
1042 &self,
1043 bucket: &str,
1044 ) -> Result<Option<ReplicationConfiguration>>;
1045
1046 async fn set_bucket_replication(
1048 &self,
1049 bucket: &str,
1050 config: ReplicationConfiguration,
1051 ) -> Result<()>;
1052
1053 async fn delete_bucket_replication(&self, bucket: &str) -> Result<()>;
1055
1056 async fn check_bucket_replication(&self, bucket: &str) -> Result<()>;
1059
1060 async fn check_bucket_replication_detailed(
1063 &self,
1064 bucket: &str,
1065 ) -> Result<ReplicationCheckResult> {
1066 self.check_bucket_replication(bucket).await?;
1067 Ok(ReplicationCheckResult::legacy_success())
1068 }
1069
1070 async fn start_bucket_replication_resync(
1072 &self,
1073 bucket: &str,
1074 options: ReplicationResyncStartOptions,
1075 ) -> Result<ReplicationResyncStartResult>;
1076
1077 async fn bucket_replication_resync_status(
1079 &self,
1080 bucket: &str,
1081 target_arn: Option<&str>,
1082 ) -> Result<ReplicationResyncStatus>;
1083
1084 async fn get_bucket_cors(&self, bucket: &str) -> Result<Vec<CorsRule>>;
1086
1087 async fn set_bucket_cors(&self, bucket: &str, rules: Vec<CorsRule>) -> Result<()>;
1089
1090 async fn delete_bucket_cors(&self, bucket: &str) -> Result<()>;
1092
1093 async fn select_object_content(
1095 &self,
1096 path: &RemotePath,
1097 options: &SelectOptions,
1098 writer: &mut (dyn AsyncWrite + Send + Unpin),
1099 ) -> Result<()>;
1100 }
1105
1106#[cfg(test)]
1107mod tests {
1108 use super::*;
1109
1110 #[test]
1111 fn create_bucket_options_reject_lock_without_versioning() {
1112 let options = CreateBucketOptions {
1113 region: Some("us-east-1".to_string()),
1114 versioning_enabled: false,
1115 object_lock_enabled: true,
1116 };
1117
1118 let error = options
1119 .validate()
1120 .expect_err("Object Lock without versioning must be rejected");
1121
1122 assert!(matches!(error, crate::Error::InvalidPath(_)));
1123 }
1124
1125 #[test]
1126 fn create_bucket_options_normalize_cli_lock_to_versioning() {
1127 let options = CreateBucketOptions::for_cli(Some("us-east-1".to_string()), false, true)
1128 .expect("CLI Object Lock options should be valid");
1129
1130 assert!(options.object_lock_enabled);
1131 assert!(options.versioning_enabled);
1132 assert_eq!(options.region.as_deref(), Some("us-east-1"));
1133 }
1134
1135 #[test]
1136 fn test_object_info_file() {
1137 let info = ObjectInfo::file("test.txt", 1024);
1138 assert_eq!(info.key, "test.txt");
1139 assert_eq!(info.size_bytes, Some(1024));
1140 assert!(!info.is_dir);
1141 assert_eq!(info.version_id, None);
1142 assert_eq!(info.source_version_id, None);
1143 assert_eq!(info.is_delete_marker, None);
1144 }
1145
1146 #[test]
1147 fn object_read_options_reject_empty_version_ids() {
1148 let error = ObjectReadOptions::for_version(Some(String::new()))
1149 .expect_err("empty version IDs must be rejected");
1150
1151 assert!(matches!(error, crate::Error::InvalidPath(_)));
1152 }
1153
1154 #[test]
1155 fn versioned_delete_targets_are_serializable_for_structured_output() {
1156 let target = ObjectVersionIdentifier {
1157 key: "reports/a.csv".to_string(),
1158 version_id: Some("v1".to_string()),
1159 is_delete_marker: true,
1160 };
1161
1162 let json = serde_json::to_value(target).expect("serialize versioned delete target");
1163 assert_eq!(json["key"], "reports/a.csv");
1164 assert_eq!(json["version_id"], "v1");
1165 assert_eq!(json["is_delete_marker"], true);
1166 }
1167
1168 #[test]
1169 fn object_info_version_fields_are_optional_and_serializable() {
1170 let current = serde_json::to_value(ObjectInfo::file("current.txt", 1))
1171 .expect("serialize current object info");
1172 assert!(current.get("version_id").is_none());
1173 assert!(current.get("source_version_id").is_none());
1174 assert!(current.get("is_delete_marker").is_none());
1175
1176 let mut copied = ObjectInfo::file("copy.txt", 1);
1177 copied.version_id = Some("destination-v2".to_string());
1178 copied.source_version_id = Some("source-v1".to_string());
1179 let copied = serde_json::to_value(copied).expect("serialize copy object info");
1180 assert_eq!(copied["version_id"], "destination-v2");
1181 assert_eq!(copied["source_version_id"], "source-v1");
1182 }
1183
1184 #[test]
1185 fn test_object_info_dir() {
1186 let info = ObjectInfo::dir("path/to/dir/");
1187 assert_eq!(info.key, "path/to/dir/");
1188 assert!(info.is_dir);
1189 assert!(info.size_bytes.is_none());
1190 }
1191
1192 #[test]
1193 fn test_object_info_bucket() {
1194 let info = ObjectInfo::bucket("my-bucket");
1195 assert_eq!(info.key, "my-bucket");
1196 assert!(info.is_dir);
1197 }
1198
1199 #[test]
1200 fn test_object_info_metadata_default_none() {
1201 let info = ObjectInfo::file("test.txt", 1024);
1202 assert!(info.metadata.is_none());
1203 }
1204
1205 #[test]
1206 fn test_object_info_metadata_set() {
1207 let mut info = ObjectInfo::file("test.txt", 1024);
1208 let mut meta = HashMap::new();
1209 meta.insert("content-disposition".to_string(), "attachment".to_string());
1210 meta.insert("custom-key".to_string(), "custom-value".to_string());
1211 info.metadata = Some(meta);
1212
1213 let metadata = info.metadata.as_ref().expect("metadata should be Some");
1214 assert_eq!(metadata.len(), 2);
1215 assert_eq!(metadata.get("content-disposition").unwrap(), "attachment");
1216 assert_eq!(metadata.get("custom-key").unwrap(), "custom-value");
1217 }
1218
1219 #[test]
1220 fn multipart_upload_serializes_stable_identity_and_server_metadata() {
1221 let upload = MultipartUpload {
1222 bucket: "archive".to_string(),
1223 key: "backups/data.tar".to_string(),
1224 upload_id: "upload-123".to_string(),
1225 initiated: Some(
1226 "2026-07-21T04:00:00Z"
1227 .parse()
1228 .expect("test timestamp should be valid"),
1229 ),
1230 size_bytes: None,
1231 storage_class: Some("STANDARD".to_string()),
1232 initiator: Some(MultipartIdentity {
1233 id: Some("user-1".to_string()),
1234 display_name: Some("backup-agent".to_string()),
1235 }),
1236 owner: None,
1237 checksum_algorithm: Some("CRC64NVME".to_string()),
1238 checksum_type: Some("FULL_OBJECT".to_string()),
1239 };
1240
1241 let value = serde_json::to_value(upload).expect("multipart upload should serialize");
1242 assert_eq!(value["bucket"], "archive");
1243 assert_eq!(value["key"], "backups/data.tar");
1244 assert_eq!(value["upload_id"], "upload-123");
1245 assert_eq!(value["initiated"], "2026-07-21T04:00:00Z");
1246 assert!(value["size_bytes"].is_null());
1247 assert_eq!(value["storage_class"], "STANDARD");
1248 assert_eq!(value["initiator"]["id"], "user-1");
1249 }
1250
1251 #[test]
1252 fn multipart_list_options_default_to_the_first_unfiltered_page() {
1253 let options = MultipartUploadListOptions::default();
1254
1255 assert!(options.prefix.is_none());
1256 assert!(options.delimiter.is_none());
1257 assert!(options.key_marker.is_none());
1258 assert!(options.upload_id_marker.is_none());
1259 assert!(options.max_uploads.is_none());
1260 }
1261}