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 async_trait::async_trait;
7use jiff::Timestamp;
8use serde::{Deserialize, Serialize};
9
10use crate::error::Result;
11use crate::path::RemotePath;
12
13/// Metadata for an object version
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ObjectVersion {
16    /// Object key
17    pub key: String,
18
19    /// Version ID
20    pub version_id: String,
21
22    /// Whether this is the latest version
23    pub is_latest: bool,
24
25    /// Whether this is a delete marker
26    pub is_delete_marker: bool,
27
28    /// Last modified timestamp
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub last_modified: Option<Timestamp>,
31
32    /// Size in bytes
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub size_bytes: Option<i64>,
35
36    /// ETag
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub etag: Option<String>,
39}
40
41/// Metadata for an object or bucket
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ObjectInfo {
44    /// Object key or bucket name
45    pub key: String,
46
47    /// Size in bytes (None for buckets)
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub size_bytes: Option<i64>,
50
51    /// Human-readable size
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub size_human: Option<String>,
54
55    /// Last modified timestamp
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub last_modified: Option<Timestamp>,
58
59    /// ETag (usually MD5 for single-part uploads)
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub etag: Option<String>,
62
63    /// Storage class
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub storage_class: Option<String>,
66
67    /// Content type
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub content_type: Option<String>,
70
71    /// Whether this is a directory/prefix
72    pub is_dir: bool,
73}
74
75impl ObjectInfo {
76    /// Create a new ObjectInfo for a file
77    pub fn file(key: impl Into<String>, size: i64) -> Self {
78        Self {
79            key: key.into(),
80            size_bytes: Some(size),
81            size_human: Some(humansize::format_size(size as u64, humansize::BINARY)),
82            last_modified: None,
83            etag: None,
84            storage_class: None,
85            content_type: None,
86            is_dir: false,
87        }
88    }
89
90    /// Create a new ObjectInfo for a directory/prefix
91    pub fn dir(key: impl Into<String>) -> Self {
92        Self {
93            key: key.into(),
94            size_bytes: None,
95            size_human: None,
96            last_modified: None,
97            etag: None,
98            storage_class: None,
99            content_type: None,
100            is_dir: true,
101        }
102    }
103
104    /// Create a new ObjectInfo for a bucket
105    pub fn bucket(name: impl Into<String>) -> Self {
106        Self {
107            key: name.into(),
108            size_bytes: None,
109            size_human: None,
110            last_modified: None,
111            etag: None,
112            storage_class: None,
113            content_type: None,
114            is_dir: true,
115        }
116    }
117}
118
119/// Result of a list operation
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ListResult {
122    /// Listed objects
123    pub items: Vec<ObjectInfo>,
124
125    /// Whether the result is truncated (more items available)
126    pub truncated: bool,
127
128    /// Continuation token for pagination
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub continuation_token: Option<String>,
131}
132
133/// Options for list operations
134#[derive(Debug, Clone, Default)]
135pub struct ListOptions {
136    /// Maximum number of keys to return per request
137    pub max_keys: Option<i32>,
138
139    /// Delimiter for grouping (usually "/")
140    pub delimiter: Option<String>,
141
142    /// Prefix to filter by
143    pub prefix: Option<String>,
144
145    /// Continuation token for pagination
146    pub continuation_token: Option<String>,
147
148    /// Whether to list recursively (ignore delimiter)
149    pub recursive: bool,
150}
151
152/// Backend capability information
153#[derive(Debug, Clone, Default)]
154pub struct Capabilities {
155    /// Supports bucket versioning
156    pub versioning: bool,
157
158    /// Supports object lock/retention
159    pub object_lock: bool,
160
161    /// Supports object tagging
162    pub tagging: bool,
163
164    /// Supports S3 Select
165    pub select: bool,
166
167    /// Supports event notifications
168    pub notifications: bool,
169}
170
171/// Trait for S3-compatible storage operations
172///
173/// This trait is implemented by the S3 adapter and can be mocked for testing.
174#[async_trait]
175pub trait ObjectStore: Send + Sync {
176    /// List buckets
177    async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
178
179    /// List objects in a bucket or prefix
180    async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
181
182    /// Get object metadata
183    async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
184
185    /// Check if a bucket exists
186    async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
187
188    /// Create a bucket
189    async fn create_bucket(&self, bucket: &str) -> Result<()>;
190
191    /// Delete a bucket
192    async fn delete_bucket(&self, bucket: &str) -> Result<()>;
193
194    /// Get backend capabilities
195    async fn capabilities(&self) -> Result<Capabilities>;
196
197    /// Get object content as bytes
198    async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
199
200    /// Upload object from bytes
201    async fn put_object(
202        &self,
203        path: &RemotePath,
204        data: Vec<u8>,
205        content_type: Option<&str>,
206    ) -> Result<ObjectInfo>;
207
208    /// Delete an object
209    async fn delete_object(&self, path: &RemotePath) -> Result<()>;
210
211    /// Delete multiple objects (batch delete)
212    async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
213
214    /// Copy object within S3 (server-side copy)
215    async fn copy_object(&self, src: &RemotePath, dst: &RemotePath) -> Result<ObjectInfo>;
216
217    /// Generate a presigned URL for an object
218    async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
219
220    /// Generate a presigned URL for uploading an object
221    async fn presign_put(
222        &self,
223        path: &RemotePath,
224        expires_secs: u64,
225        content_type: Option<&str>,
226    ) -> Result<String>;
227
228    // Phase 5: Optional operations (capability-dependent)
229
230    /// Get bucket versioning status
231    async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
232
233    /// Set bucket versioning status
234    async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
235
236    /// List object versions
237    async fn list_object_versions(
238        &self,
239        path: &RemotePath,
240        max_keys: Option<i32>,
241    ) -> Result<Vec<ObjectVersion>>;
242
243    /// Get object tags
244    async fn get_object_tags(
245        &self,
246        path: &RemotePath,
247    ) -> Result<std::collections::HashMap<String, String>>;
248
249    /// Get bucket tags
250    async fn get_bucket_tags(
251        &self,
252        bucket: &str,
253    ) -> Result<std::collections::HashMap<String, String>>;
254
255    /// Set object tags
256    async fn set_object_tags(
257        &self,
258        path: &RemotePath,
259        tags: std::collections::HashMap<String, String>,
260    ) -> Result<()>;
261
262    /// Set bucket tags
263    async fn set_bucket_tags(
264        &self,
265        bucket: &str,
266        tags: std::collections::HashMap<String, String>,
267    ) -> Result<()>;
268
269    /// Delete object tags
270    async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
271
272    /// Delete bucket tags
273    async fn delete_bucket_tags(&self, bucket: &str) -> Result<()>;
274    // async fn get_versioning(&self, bucket: &str) -> Result<bool>;
275    // async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
276    // async fn get_tags(&self, path: &RemotePath) -> Result<HashMap<String, String>>;
277    // async fn set_tags(&self, path: &RemotePath, tags: HashMap<String, String>) -> Result<()>;
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_object_info_file() {
286        let info = ObjectInfo::file("test.txt", 1024);
287        assert_eq!(info.key, "test.txt");
288        assert_eq!(info.size_bytes, Some(1024));
289        assert!(!info.is_dir);
290    }
291
292    #[test]
293    fn test_object_info_dir() {
294        let info = ObjectInfo::dir("path/to/dir/");
295        assert_eq!(info.key, "path/to/dir/");
296        assert!(info.is_dir);
297        assert!(info.size_bytes.is_none());
298    }
299
300    #[test]
301    fn test_object_info_bucket() {
302        let info = ObjectInfo::bucket("my-bucket");
303        assert_eq!(info.key, "my-bucket");
304        assert!(info.is_dir);
305    }
306}