Skip to main content

rc_core/
traits.rs

1//! ObjectStore trait definition
2//!
3//! This trait defines the interface for S3-compatible storage operations.
4//! It allows the CLI to be decoupled from the specific S3 SDK implementation.
5
6use 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/// Requested behavior for bucket creation.
34#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
35pub struct CreateBucketOptions {
36    /// Explicit S3 location constraint. `None` omits the request body.
37    pub region: Option<String>,
38    /// Whether the resulting bucket must have versioning enabled.
39    pub versioning_enabled: bool,
40    /// Whether Object Lock must be enabled in the create request.
41    pub object_lock_enabled: bool,
42}
43
44impl CreateBucketOptions {
45    /// Build CLI options while applying Object Lock's required versioning invariant.
46    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    /// Reject request states that cannot produce the promised bucket state.
61    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/// Metadata for an object version
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ObjectVersion {
83    /// Object key
84    pub key: String,
85
86    /// Version ID
87    pub version_id: String,
88
89    /// Whether this is the latest version
90    pub is_latest: bool,
91
92    /// Whether this is a delete marker
93    pub is_delete_marker: bool,
94
95    /// Last modified timestamp
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub last_modified: Option<Timestamp>,
98
99    /// Size in bytes
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub size_bytes: Option<i64>,
102
103    /// ETag
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub etag: Option<String>,
106}
107
108/// Result of an object version list operation
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ObjectVersionListResult {
111    /// Listed object versions and delete markers
112    pub items: Vec<ObjectVersion>,
113
114    /// Whether the result is truncated (more items available)
115    pub truncated: bool,
116
117    /// Continuation key marker for pagination
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub continuation_token: Option<String>,
120
121    /// Continuation version marker for pagination
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub version_id_marker: Option<String>,
124}
125
126/// Options for selecting an object version during read and metadata operations.
127#[derive(Debug, Clone, Default, PartialEq, Eq)]
128pub struct ObjectReadOptions {
129    /// Exact object version to select. `None` selects the current object.
130    pub version_id: Option<String>,
131}
132
133/// Request options for a server-side object copy.
134#[derive(Debug, Clone, Default, PartialEq, Eq)]
135pub struct CopyObjectOptions {
136    /// Exact historical source version to copy. `None` selects the current source.
137    pub source_version_id: Option<String>,
138}
139
140impl CopyObjectOptions {
141    /// Build copy options while rejecting an ambiguous empty source version identifier.
142    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    /// Build read options while rejecting ambiguous empty version identifiers.
154    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/// Pagination options for listing object versions and delete markers.
163#[derive(Debug, Clone, Default, PartialEq, Eq)]
164pub struct ListObjectVersionsOptions {
165    /// Maximum number of entries to return.
166    pub max_keys: Option<i32>,
167    /// Key marker returned by the previous page.
168    pub key_marker: Option<String>,
169    /// Version marker returned by the previous page.
170    pub version_id_marker: Option<String>,
171}
172
173/// Request-level options for object deletion.
174#[derive(Debug, Clone, Default, PartialEq, Eq)]
175pub struct DeleteRequestOptions {
176    /// Exact object version to delete. `None` targets the current object state.
177    pub version_id: Option<String>,
178    /// Explicitly bypass Object Lock governance retention.
179    pub bypass_governance: bool,
180    /// Ask RustFS to permanently delete data instead of creating delete markers.
181    pub force_delete: bool,
182}
183
184/// An object key and optional historical version selected for deletion.
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct ObjectVersionIdentifier {
187    /// Object key.
188    pub key: String,
189    /// Exact version to delete, when version-aware deletion is requested.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub version_id: Option<String>,
192    /// Whether the selected version was listed as a delete marker.
193    pub is_delete_marker: bool,
194}
195
196/// A version-aware delete result returned by the object store.
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
198pub struct DeletedObject {
199    /// Deleted object key.
200    pub key: String,
201    /// Deleted object version, when reported by the backend.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub version_id: Option<String>,
204    /// Whether the deleted entry is a delete marker.
205    pub is_delete_marker: bool,
206}
207
208/// A per-object failure returned by a multi-object delete request.
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210pub struct DeleteObjectFailure {
211    /// Object key that could not be deleted.
212    pub key: String,
213    /// Requested version, when present.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub version_id: Option<String>,
216    /// S3 error code, when provided.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub code: Option<String>,
219    /// Backend error message, when provided.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub message: Option<String>,
222}
223
224/// Result of deleting multiple version-aware object identifiers.
225#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
226pub struct DeleteObjectsResult {
227    /// Successfully deleted entries.
228    pub deleted: Vec<DeletedObject>,
229    /// Entries rejected by the backend.
230    pub failures: Vec<DeleteObjectFailure>,
231}
232
233/// Metadata for an object or bucket
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct ObjectInfo {
236    /// Object key or bucket name
237    pub key: String,
238
239    /// Size in bytes (None for buckets)
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub size_bytes: Option<i64>,
242
243    /// Human-readable size
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub size_human: Option<String>,
246
247    /// Last modified timestamp
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub last_modified: Option<Timestamp>,
250
251    /// ETag (usually MD5 for single-part uploads)
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub etag: Option<String>,
254
255    /// Storage class
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub storage_class: Option<String>,
258
259    /// Content type
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub content_type: Option<String>,
262
263    /// User-defined metadata
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub metadata: Option<HashMap<String, String>>,
266
267    /// Object version selected or created by the operation.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub version_id: Option<String>,
270
271    /// Source object version used by a copy operation.
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub source_version_id: Option<String>,
274
275    /// Whether the selected version is a delete marker.
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub is_delete_marker: Option<bool>,
278
279    /// Whether this is a directory/prefix
280    pub is_dir: bool,
281}
282
283impl ObjectInfo {
284    /// Create a new ObjectInfo for a file
285    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    /// Create a new ObjectInfo for a directory/prefix
303    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    /// Create a new ObjectInfo for a bucket
321    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/// Result of a list operation
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ListResult {
342    /// Listed objects
343    pub items: Vec<ObjectInfo>,
344
345    /// Whether the result is truncated (more items available)
346    pub truncated: bool,
347
348    /// Continuation token for pagination
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub continuation_token: Option<String>,
351}
352
353/// Options for list operations
354#[derive(Debug, Clone, Default)]
355pub struct ListOptions {
356    /// Maximum number of keys to return per request
357    pub max_keys: Option<i32>,
358
359    /// Delimiter for grouping (usually "/")
360    pub delimiter: Option<String>,
361
362    /// Prefix to filter by
363    pub prefix: Option<String>,
364
365    /// Continuation token for pagination
366    pub continuation_token: Option<String>,
367
368    /// Whether to list recursively (ignore delimiter)
369    pub recursive: bool,
370}
371
372/// S3 identity attached to an incomplete multipart upload.
373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
374pub struct MultipartIdentity {
375    /// Stable account or principal identifier, when returned by the server.
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub id: Option<String>,
378
379    /// Human-readable principal name, when returned by the server.
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub display_name: Option<String>,
382}
383
384/// Metadata for an incomplete multipart upload.
385///
386/// `size_bytes` is nullable because the S3 list-multipart-uploads response does not expose the
387/// uploaded part total. Keeping the field in the typed record makes the representation compatible
388/// with richer backends and the output v3 mapping without issuing one request per upload.
389#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
390pub struct MultipartUpload {
391    /// Bucket containing the pending upload.
392    pub bucket: String,
393
394    /// Object key being uploaded.
395    pub key: String,
396
397    /// Server-assigned multipart upload identifier.
398    pub upload_id: String,
399
400    /// Time at which the upload was initiated, when returned by the server.
401    pub initiated: Option<Timestamp>,
402
403    /// Bytes uploaded so far, when the backend provides this value.
404    pub size_bytes: Option<i64>,
405
406    /// Requested storage class, when returned by the server.
407    pub storage_class: Option<String>,
408
409    /// Principal that initiated the upload.
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub initiator: Option<MultipartIdentity>,
412
413    /// Owner of the pending object.
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub owner: Option<MultipartIdentity>,
416
417    /// Checksum algorithm selected when the upload was created.
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub checksum_algorithm: Option<String>,
420
421    /// Checksum aggregation type selected when the upload was created.
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub checksum_type: Option<String>,
424}
425
426/// Options for one page of incomplete multipart uploads.
427#[derive(Debug, Clone, Default, PartialEq, Eq)]
428pub struct MultipartUploadListOptions {
429    /// Only return object keys beginning with this prefix.
430    pub prefix: Option<String>,
431
432    /// Group keys containing this delimiter into common prefixes.
433    pub delimiter: Option<String>,
434
435    /// Key marker returned by the previous page.
436    pub key_marker: Option<String>,
437
438    /// Upload ID marker returned by the previous page.
439    pub upload_id_marker: Option<String>,
440
441    /// Maximum number of uploads and common prefixes to return.
442    pub max_uploads: Option<i32>,
443}
444
445/// Result of one incomplete multipart upload listing page.
446#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447pub struct MultipartUploadListResult {
448    /// Uploads returned on this page.
449    pub uploads: Vec<MultipartUpload>,
450
451    /// Grouped prefixes returned when a delimiter was supplied.
452    #[serde(default, skip_serializing_if = "Vec::is_empty")]
453    pub common_prefixes: Vec<String>,
454
455    /// Whether another page is available.
456    pub truncated: bool,
457
458    /// Key marker for the next page.
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub next_key_marker: Option<String>,
461
462    /// Upload ID marker for the next page.
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub next_upload_id_marker: Option<String>,
465}
466
467/// Identifies one incomplete multipart upload to abort.
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct AbortMultipartUploadRequest {
470    /// Bucket containing the upload.
471    pub bucket: String,
472
473    /// Object key being uploaded.
474    pub key: String,
475
476    /// Server-assigned multipart upload identifier.
477    pub upload_id: String,
478}
479
480/// Backend capability information
481#[derive(Debug, Clone, Default)]
482pub struct Capabilities {
483    /// Supports bucket versioning
484    pub versioning: bool,
485
486    /// Supports object lock/retention
487    pub object_lock: bool,
488
489    /// Supports object tagging
490    pub tagging: bool,
491
492    /// Supports anonymous bucket access policies
493    pub anonymous: bool,
494
495    /// S3 Select (`SelectObjectContent`).
496    ///
497    /// This remains `false` in generic capability hints because support is determined by issuing
498    /// a real request against the target object.
499    pub select: bool,
500
501    /// Supports event notifications
502    pub notifications: bool,
503
504    /// Supports lifecycle configuration
505    pub lifecycle: bool,
506
507    /// Supports bucket replication
508    pub replication: bool,
509
510    /// Supports bucket CORS configuration
511    pub cors: bool,
512}
513
514/// Bucket notification target type
515#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
516#[serde(rename_all = "lowercase")]
517pub enum NotificationTarget {
518    /// SQS queue target
519    Queue,
520    /// SNS topic target
521    Topic,
522    /// Lambda function target
523    Lambda,
524}
525
526/// Bucket notification rule
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
528pub struct BucketNotification {
529    /// Optional rule id
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub id: Option<String>,
532    /// Notification target type
533    pub target: NotificationTarget,
534    /// Target ARN
535    pub arn: String,
536    /// Event patterns
537    pub events: Vec<String>,
538    /// Optional key prefix filter
539    #[serde(skip_serializing_if = "Option::is_none")]
540    pub prefix: Option<String>,
541    /// Optional key suffix filter
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub suffix: Option<String>,
544}
545
546/// Trait for S3-compatible storage operations
547///
548/// This trait is implemented by the S3 adapter and can be mocked for testing.
549#[async_trait]
550pub trait ObjectStore: Send + Sync {
551    /// List buckets
552    async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
553
554    /// List objects in a bucket or prefix
555    async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
556
557    /// List one page of incomplete multipart uploads.
558    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    /// Abort one incomplete multipart upload.
570    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    /// Get object metadata
578    async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
579
580    /// Get metadata for the current object or an exact historical version.
581    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    /// Get metadata with checksum-mode and SSE-C support when implemented by the backend.
595    ///
596    /// The default preserves legacy version selection and rejects advanced options explicitly.
597    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    /// Read complete transfer metadata without changing ObjectInfo's stable output contract.
607    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    /// Check if a bucket exists
619    async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
620
621    /// Create a bucket
622    async fn create_bucket(&self, bucket: &str) -> Result<()>;
623
624    /// Create a bucket with explicit region, versioning, and Object Lock intent.
625    ///
626    /// The default preserves existing implementations for the option-free request and rejects
627    /// advanced behavior instead of silently ignoring it.
628    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    /// Return the effective location reported by the service.
643    ///
644    /// `None` is the S3 representation for the default `us-east-1` location.
645    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    /// Delete a bucket
652    async fn delete_bucket(&self, bucket: &str) -> Result<()>;
653
654    /// Get backend capabilities
655    async fn capabilities(&self) -> Result<Capabilities>;
656
657    /// Get object content as bytes
658    async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
659
660    /// Get current object content or an exact historical version as bytes.
661    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    /// Read an object with checksum-mode and SSE-C support when implemented by the backend.
675    ///
676    /// The default preserves legacy version selection and rejects advanced options explicitly.
677    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    /// Stream current object content or an exact historical version to a writer.
687    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    /// Stream an object with checksum-mode and SSE-C support when implemented by the backend.
705    ///
706    /// The default preserves legacy version selection and rejects advanced options explicitly.
707    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    /// Upload object from bytes
720    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    /// Upload an object with complete transfer-fidelity options.
729    ///
730    /// The default delegates Content-Type and managed encryption to the legacy API and rejects
731    /// every option that cannot be represented there.
732    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    /// Delete an object
743    async fn delete_object(&self, path: &RemotePath) -> Result<()>;
744
745    /// Delete the current object state or one exact version with explicit request options.
746    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    /// Delete multiple objects (batch delete)
766    async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
767
768    /// Delete exact object versions and delete markers in one request.
769    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    /// Copy object within S3 (server-side copy)
781    async fn copy_object(
782        &self,
783        src: &RemotePath,
784        dst: &RemotePath,
785        encryption: Option<&ObjectEncryptionRequest>,
786    ) -> Result<ObjectInfo>;
787
788    /// Copy the current object or one exact historical source version.
789    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    /// Copy an object with explicit metadata, tags, checksum, SSE-C, and lock intent.
806    ///
807    /// The default preserves legacy source-version and managed-encryption behavior while
808    /// rejecting every advanced option that the original API cannot represent.
809    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    /// Copy an object with S3's multipart server-side copy lifecycle.
821    ///
822    /// Backends that do not implement multipart copy fail explicitly. The
823    /// callback receives cumulative logical bytes after each successful part.
824    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    /// Copy an object through the multipart lifecycle with transfer-fidelity options.
839    ///
840    /// The default delegates only when all requested fidelity can be represented by the legacy
841    /// multipart API. Implementations must override this method before accepting advanced fields.
842    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    /// Generate a presigned URL for an object
858    async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
859
860    /// Generate a presigned URL for uploading an object
861    async fn presign_put(
862        &self,
863        path: &RemotePath,
864        expires_secs: u64,
865        content_type: Option<&str>,
866    ) -> Result<String>;
867
868    // Phase 5: Optional operations (capability-dependent)
869
870    /// Get bucket versioning status
871    async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
872
873    /// Set bucket versioning status
874    async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
875
876    /// Get a bucket's Object Lock configuration.
877    ///
878    /// `None` means the bucket exists but has no Object Lock configuration.
879    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    /// Update an Object Lock enabled bucket's configuration.
889    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    /// Get retention applied to the selected object version.
900    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    /// Set or clear retention on the selected object version.
911    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    /// Get legal-hold status for the selected object version.
923    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    /// Set legal-hold status for the selected object version.
934    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    /// Get bucket default encryption. Returns None when encryption is not configured.
946    async fn get_bucket_encryption(&self, bucket: &str) -> Result<Option<BucketEncryption>>;
947
948    /// Set bucket default encryption.
949    async fn set_bucket_encryption(&self, bucket: &str, encryption: BucketEncryption)
950    -> Result<()>;
951
952    /// Delete bucket default encryption.
953    async fn delete_bucket_encryption(&self, bucket: &str) -> Result<()>;
954
955    /// List object versions
956    async fn list_object_versions(
957        &self,
958        path: &RemotePath,
959        max_keys: Option<i32>,
960    ) -> Result<Vec<ObjectVersion>>;
961
962    /// List one page of object versions and delete markers with both S3 pagination markers.
963    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    /// Get object tags
974    async fn get_object_tags(
975        &self,
976        path: &RemotePath,
977    ) -> Result<std::collections::HashMap<String, String>>;
978
979    /// Get bucket tags
980    async fn get_bucket_tags(
981        &self,
982        bucket: &str,
983    ) -> Result<std::collections::HashMap<String, String>>;
984
985    /// Set object tags
986    async fn set_object_tags(
987        &self,
988        path: &RemotePath,
989        tags: std::collections::HashMap<String, String>,
990    ) -> Result<()>;
991
992    /// Set bucket tags
993    async fn set_bucket_tags(
994        &self,
995        bucket: &str,
996        tags: std::collections::HashMap<String, String>,
997    ) -> Result<()>;
998
999    /// Delete object tags
1000    async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
1001
1002    /// Delete bucket tags
1003    async fn delete_bucket_tags(&self, bucket: &str) -> Result<()>;
1004
1005    /// Get bucket policy as raw JSON string. Returns `None` when no policy exists.
1006    async fn get_bucket_policy(&self, bucket: &str) -> Result<Option<String>>;
1007
1008    /// Replace bucket policy using raw JSON string.
1009    async fn set_bucket_policy(&self, bucket: &str, policy: &str) -> Result<()>;
1010
1011    /// Remove bucket policy (set anonymous access to private).
1012    async fn delete_bucket_policy(&self, bucket: &str) -> Result<()>;
1013
1014    /// Get bucket notification configuration as flat rules.
1015    async fn get_bucket_notifications(&self, bucket: &str) -> Result<Vec<BucketNotification>>;
1016
1017    /// Replace bucket notification configuration with flat rules.
1018    async fn set_bucket_notifications(
1019        &self,
1020        bucket: &str,
1021        notifications: Vec<BucketNotification>,
1022    ) -> Result<()>;
1023
1024    // Lifecycle operations (capability-dependent)
1025
1026    /// Get bucket lifecycle rules. Returns empty vec if no lifecycle config exists.
1027    async fn get_bucket_lifecycle(&self, bucket: &str) -> Result<Vec<LifecycleRule>>;
1028
1029    /// Set bucket lifecycle configuration (replaces all rules).
1030    async fn set_bucket_lifecycle(&self, bucket: &str, rules: Vec<LifecycleRule>) -> Result<()>;
1031
1032    /// Delete bucket lifecycle configuration.
1033    async fn delete_bucket_lifecycle(&self, bucket: &str) -> Result<()>;
1034
1035    /// Restore a transitioned (archived) object.
1036    async fn restore_object(&self, path: &RemotePath, days: i32) -> Result<()>;
1037
1038    // Replication operations (capability-dependent)
1039
1040    /// Get bucket replication configuration. Returns None if not configured.
1041    async fn get_bucket_replication(
1042        &self,
1043        bucket: &str,
1044    ) -> Result<Option<ReplicationConfiguration>>;
1045
1046    /// Set bucket replication configuration.
1047    async fn set_bucket_replication(
1048        &self,
1049        bucket: &str,
1050        config: ReplicationConfiguration,
1051    ) -> Result<()>;
1052
1053    /// Delete bucket replication configuration.
1054    async fn delete_bucket_replication(&self, bucket: &str) -> Result<()>;
1055
1056    /// Actively validate configured replication targets using the legacy
1057    /// success/failure contract.
1058    async fn check_bucket_replication(&self, bucket: &str) -> Result<()>;
1059
1060    /// Actively validate configured replication targets and retain structured
1061    /// per-target and cleanup outcomes when the server provides them.
1062    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    /// Start a server-side bucket replication resync.
1071    async fn start_bucket_replication_resync(
1072        &self,
1073        bucket: &str,
1074        options: ReplicationResyncStartOptions,
1075    ) -> Result<ReplicationResyncStartResult>;
1076
1077    /// Read persisted server-side bucket replication resync status.
1078    async fn bucket_replication_resync_status(
1079        &self,
1080        bucket: &str,
1081        target_arn: Option<&str>,
1082    ) -> Result<ReplicationResyncStatus>;
1083
1084    /// Get bucket CORS rules. Returns empty vec if no CORS config exists.
1085    async fn get_bucket_cors(&self, bucket: &str) -> Result<Vec<CorsRule>>;
1086
1087    /// Set bucket CORS configuration (replaces all rules).
1088    async fn set_bucket_cors(&self, bucket: &str, rules: Vec<CorsRule>) -> Result<()>;
1089
1090    /// Delete bucket CORS configuration.
1091    async fn delete_bucket_cors(&self, bucket: &str) -> Result<()>;
1092
1093    /// Run S3 Select on an object and stream result payloads to `writer`.
1094    async fn select_object_content(
1095        &self,
1096        path: &RemotePath,
1097        options: &SelectOptions,
1098        writer: &mut (dyn AsyncWrite + Send + Unpin),
1099    ) -> Result<()>;
1100    // async fn get_versioning(&self, bucket: &str) -> Result<bool>;
1101    // async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
1102    // async fn get_tags(&self, path: &RemotePath) -> Result<HashMap<String, String>>;
1103    // async fn set_tags(&self, path: &RemotePath, tags: HashMap<String, String>) -> Result<()>;
1104}
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}