1use std::collections::HashMap;
7
8use async_trait::async_trait;
9use jiff::Timestamp;
10use serde::{Deserialize, Serialize};
11
12use crate::error::Result;
13use crate::lifecycle::LifecycleRule;
14use crate::path::RemotePath;
15use crate::replication::ReplicationConfiguration;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ObjectVersion {
20 pub key: String,
22
23 pub version_id: String,
25
26 pub is_latest: bool,
28
29 pub is_delete_marker: bool,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub last_modified: Option<Timestamp>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub size_bytes: Option<i64>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub etag: Option<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ObjectInfo {
48 pub key: String,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub size_bytes: Option<i64>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub size_human: Option<String>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub last_modified: Option<Timestamp>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub etag: Option<String>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub storage_class: Option<String>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub content_type: Option<String>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub metadata: Option<HashMap<String, String>>,
78
79 pub is_dir: bool,
81}
82
83impl ObjectInfo {
84 pub fn file(key: impl Into<String>, size: i64) -> Self {
86 Self {
87 key: key.into(),
88 size_bytes: Some(size),
89 size_human: Some(humansize::format_size(size as u64, humansize::BINARY)),
90 last_modified: None,
91 etag: None,
92 storage_class: None,
93 content_type: None,
94 metadata: None,
95 is_dir: false,
96 }
97 }
98
99 pub fn dir(key: impl Into<String>) -> Self {
101 Self {
102 key: key.into(),
103 size_bytes: None,
104 size_human: None,
105 last_modified: None,
106 etag: None,
107 storage_class: None,
108 content_type: None,
109 metadata: None,
110 is_dir: true,
111 }
112 }
113
114 pub fn bucket(name: impl Into<String>) -> Self {
116 Self {
117 key: name.into(),
118 size_bytes: None,
119 size_human: None,
120 last_modified: None,
121 etag: None,
122 storage_class: None,
123 content_type: None,
124 metadata: None,
125 is_dir: true,
126 }
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ListResult {
133 pub items: Vec<ObjectInfo>,
135
136 pub truncated: bool,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub continuation_token: Option<String>,
142}
143
144#[derive(Debug, Clone, Default)]
146pub struct ListOptions {
147 pub max_keys: Option<i32>,
149
150 pub delimiter: Option<String>,
152
153 pub prefix: Option<String>,
155
156 pub continuation_token: Option<String>,
158
159 pub recursive: bool,
161}
162
163#[derive(Debug, Clone, Default)]
165pub struct Capabilities {
166 pub versioning: bool,
168
169 pub object_lock: bool,
171
172 pub tagging: bool,
174
175 pub anonymous: bool,
177
178 pub select: bool,
180
181 pub notifications: bool,
183
184 pub lifecycle: bool,
186
187 pub replication: bool,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "lowercase")]
194pub enum NotificationTarget {
195 Queue,
197 Topic,
199 Lambda,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct BucketNotification {
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub id: Option<String>,
209 pub target: NotificationTarget,
211 pub arn: String,
213 pub events: Vec<String>,
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub prefix: Option<String>,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub suffix: Option<String>,
221}
222
223#[async_trait]
227pub trait ObjectStore: Send + Sync {
228 async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
230
231 async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
233
234 async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
236
237 async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
239
240 async fn create_bucket(&self, bucket: &str) -> Result<()>;
242
243 async fn delete_bucket(&self, bucket: &str) -> Result<()>;
245
246 async fn capabilities(&self) -> Result<Capabilities>;
248
249 async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
251
252 async fn put_object(
254 &self,
255 path: &RemotePath,
256 data: Vec<u8>,
257 content_type: Option<&str>,
258 ) -> Result<ObjectInfo>;
259
260 async fn delete_object(&self, path: &RemotePath) -> Result<()>;
262
263 async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
265
266 async fn copy_object(&self, src: &RemotePath, dst: &RemotePath) -> Result<ObjectInfo>;
268
269 async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
271
272 async fn presign_put(
274 &self,
275 path: &RemotePath,
276 expires_secs: u64,
277 content_type: Option<&str>,
278 ) -> Result<String>;
279
280 async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
284
285 async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
287
288 async fn list_object_versions(
290 &self,
291 path: &RemotePath,
292 max_keys: Option<i32>,
293 ) -> Result<Vec<ObjectVersion>>;
294
295 async fn get_object_tags(
297 &self,
298 path: &RemotePath,
299 ) -> Result<std::collections::HashMap<String, String>>;
300
301 async fn get_bucket_tags(
303 &self,
304 bucket: &str,
305 ) -> Result<std::collections::HashMap<String, String>>;
306
307 async fn set_object_tags(
309 &self,
310 path: &RemotePath,
311 tags: std::collections::HashMap<String, String>,
312 ) -> Result<()>;
313
314 async fn set_bucket_tags(
316 &self,
317 bucket: &str,
318 tags: std::collections::HashMap<String, String>,
319 ) -> Result<()>;
320
321 async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
323
324 async fn delete_bucket_tags(&self, bucket: &str) -> Result<()>;
326
327 async fn get_bucket_policy(&self, bucket: &str) -> Result<Option<String>>;
329
330 async fn set_bucket_policy(&self, bucket: &str, policy: &str) -> Result<()>;
332
333 async fn delete_bucket_policy(&self, bucket: &str) -> Result<()>;
335
336 async fn get_bucket_notifications(&self, bucket: &str) -> Result<Vec<BucketNotification>>;
338
339 async fn set_bucket_notifications(
341 &self,
342 bucket: &str,
343 notifications: Vec<BucketNotification>,
344 ) -> Result<()>;
345
346 async fn get_bucket_lifecycle(&self, bucket: &str) -> Result<Vec<LifecycleRule>>;
350
351 async fn set_bucket_lifecycle(&self, bucket: &str, rules: Vec<LifecycleRule>) -> Result<()>;
353
354 async fn delete_bucket_lifecycle(&self, bucket: &str) -> Result<()>;
356
357 async fn restore_object(&self, path: &RemotePath, days: i32) -> Result<()>;
359
360 async fn get_bucket_replication(
364 &self,
365 bucket: &str,
366 ) -> Result<Option<ReplicationConfiguration>>;
367
368 async fn set_bucket_replication(
370 &self,
371 bucket: &str,
372 config: ReplicationConfiguration,
373 ) -> Result<()>;
374
375 async fn delete_bucket_replication(&self, bucket: &str) -> Result<()>;
377 }
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn test_object_info_file() {
389 let info = ObjectInfo::file("test.txt", 1024);
390 assert_eq!(info.key, "test.txt");
391 assert_eq!(info.size_bytes, Some(1024));
392 assert!(!info.is_dir);
393 }
394
395 #[test]
396 fn test_object_info_dir() {
397 let info = ObjectInfo::dir("path/to/dir/");
398 assert_eq!(info.key, "path/to/dir/");
399 assert!(info.is_dir);
400 assert!(info.size_bytes.is_none());
401 }
402
403 #[test]
404 fn test_object_info_bucket() {
405 let info = ObjectInfo::bucket("my-bucket");
406 assert_eq!(info.key, "my-bucket");
407 assert!(info.is_dir);
408 }
409
410 #[test]
411 fn test_object_info_metadata_default_none() {
412 let info = ObjectInfo::file("test.txt", 1024);
413 assert!(info.metadata.is_none());
414 }
415
416 #[test]
417 fn test_object_info_metadata_set() {
418 let mut info = ObjectInfo::file("test.txt", 1024);
419 let mut meta = HashMap::new();
420 meta.insert("content-disposition".to_string(), "attachment".to_string());
421 meta.insert("custom-key".to_string(), "custom-value".to_string());
422 info.metadata = Some(meta);
423
424 let metadata = info.metadata.as_ref().expect("metadata should be Some");
425 assert_eq!(metadata.len(), 2);
426 assert_eq!(metadata.get("content-disposition").unwrap(), "attachment");
427 assert_eq!(metadata.get("custom-key").unwrap(), "custom-value");
428 }
429}