1use std::collections::HashMap;
7
8use async_trait::async_trait;
9use jiff::Timestamp;
10use serde::{Deserialize, Serialize};
11use tokio::io::AsyncWrite;
12
13use crate::cors::CorsRule;
14use crate::error::Result;
15use crate::lifecycle::LifecycleRule;
16use crate::path::RemotePath;
17use crate::replication::ReplicationConfiguration;
18use crate::select::SelectOptions;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ObjectVersion {
23 pub key: String,
25
26 pub version_id: String,
28
29 pub is_latest: bool,
31
32 pub is_delete_marker: bool,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub last_modified: Option<Timestamp>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub size_bytes: Option<i64>,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub etag: Option<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ObjectVersionListResult {
51 pub items: Vec<ObjectVersion>,
53
54 pub truncated: bool,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub continuation_token: Option<String>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub version_id_marker: Option<String>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ObjectInfo {
69 pub key: String,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub size_bytes: Option<i64>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub size_human: Option<String>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub last_modified: Option<Timestamp>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub etag: Option<String>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub storage_class: Option<String>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub content_type: Option<String>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub metadata: Option<HashMap<String, String>>,
99
100 pub is_dir: bool,
102}
103
104impl ObjectInfo {
105 pub fn file(key: impl Into<String>, size: i64) -> Self {
107 Self {
108 key: key.into(),
109 size_bytes: Some(size),
110 size_human: Some(humansize::format_size(size as u64, humansize::BINARY)),
111 last_modified: None,
112 etag: None,
113 storage_class: None,
114 content_type: None,
115 metadata: None,
116 is_dir: false,
117 }
118 }
119
120 pub fn dir(key: impl Into<String>) -> Self {
122 Self {
123 key: key.into(),
124 size_bytes: None,
125 size_human: None,
126 last_modified: None,
127 etag: None,
128 storage_class: None,
129 content_type: None,
130 metadata: None,
131 is_dir: true,
132 }
133 }
134
135 pub fn bucket(name: impl Into<String>) -> Self {
137 Self {
138 key: name.into(),
139 size_bytes: None,
140 size_human: None,
141 last_modified: None,
142 etag: None,
143 storage_class: None,
144 content_type: None,
145 metadata: None,
146 is_dir: true,
147 }
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ListResult {
154 pub items: Vec<ObjectInfo>,
156
157 pub truncated: bool,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub continuation_token: Option<String>,
163}
164
165#[derive(Debug, Clone, Default)]
167pub struct ListOptions {
168 pub max_keys: Option<i32>,
170
171 pub delimiter: Option<String>,
173
174 pub prefix: Option<String>,
176
177 pub continuation_token: Option<String>,
179
180 pub recursive: bool,
182}
183
184#[derive(Debug, Clone, Default)]
186pub struct Capabilities {
187 pub versioning: bool,
189
190 pub object_lock: bool,
192
193 pub tagging: bool,
195
196 pub anonymous: bool,
198
199 pub select: bool,
204
205 pub notifications: bool,
207
208 pub lifecycle: bool,
210
211 pub replication: bool,
213
214 pub cors: bool,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(rename_all = "lowercase")]
221pub enum NotificationTarget {
222 Queue,
224 Topic,
226 Lambda,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct BucketNotification {
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub id: Option<String>,
236 pub target: NotificationTarget,
238 pub arn: String,
240 pub events: Vec<String>,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub prefix: Option<String>,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub suffix: Option<String>,
248}
249
250#[async_trait]
254pub trait ObjectStore: Send + Sync {
255 async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
257
258 async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
260
261 async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
263
264 async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
266
267 async fn create_bucket(&self, bucket: &str) -> Result<()>;
269
270 async fn delete_bucket(&self, bucket: &str) -> Result<()>;
272
273 async fn capabilities(&self) -> Result<Capabilities>;
275
276 async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
278
279 async fn put_object(
281 &self,
282 path: &RemotePath,
283 data: Vec<u8>,
284 content_type: Option<&str>,
285 ) -> Result<ObjectInfo>;
286
287 async fn delete_object(&self, path: &RemotePath) -> Result<()>;
289
290 async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
292
293 async fn copy_object(&self, src: &RemotePath, dst: &RemotePath) -> Result<ObjectInfo>;
295
296 async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
298
299 async fn presign_put(
301 &self,
302 path: &RemotePath,
303 expires_secs: u64,
304 content_type: Option<&str>,
305 ) -> Result<String>;
306
307 async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
311
312 async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
314
315 async fn list_object_versions(
317 &self,
318 path: &RemotePath,
319 max_keys: Option<i32>,
320 ) -> Result<Vec<ObjectVersion>>;
321
322 async fn get_object_tags(
324 &self,
325 path: &RemotePath,
326 ) -> Result<std::collections::HashMap<String, String>>;
327
328 async fn get_bucket_tags(
330 &self,
331 bucket: &str,
332 ) -> Result<std::collections::HashMap<String, String>>;
333
334 async fn set_object_tags(
336 &self,
337 path: &RemotePath,
338 tags: std::collections::HashMap<String, String>,
339 ) -> Result<()>;
340
341 async fn set_bucket_tags(
343 &self,
344 bucket: &str,
345 tags: std::collections::HashMap<String, String>,
346 ) -> Result<()>;
347
348 async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
350
351 async fn delete_bucket_tags(&self, bucket: &str) -> Result<()>;
353
354 async fn get_bucket_policy(&self, bucket: &str) -> Result<Option<String>>;
356
357 async fn set_bucket_policy(&self, bucket: &str, policy: &str) -> Result<()>;
359
360 async fn delete_bucket_policy(&self, bucket: &str) -> Result<()>;
362
363 async fn get_bucket_notifications(&self, bucket: &str) -> Result<Vec<BucketNotification>>;
365
366 async fn set_bucket_notifications(
368 &self,
369 bucket: &str,
370 notifications: Vec<BucketNotification>,
371 ) -> Result<()>;
372
373 async fn get_bucket_lifecycle(&self, bucket: &str) -> Result<Vec<LifecycleRule>>;
377
378 async fn set_bucket_lifecycle(&self, bucket: &str, rules: Vec<LifecycleRule>) -> Result<()>;
380
381 async fn delete_bucket_lifecycle(&self, bucket: &str) -> Result<()>;
383
384 async fn restore_object(&self, path: &RemotePath, days: i32) -> Result<()>;
386
387 async fn get_bucket_replication(
391 &self,
392 bucket: &str,
393 ) -> Result<Option<ReplicationConfiguration>>;
394
395 async fn set_bucket_replication(
397 &self,
398 bucket: &str,
399 config: ReplicationConfiguration,
400 ) -> Result<()>;
401
402 async fn delete_bucket_replication(&self, bucket: &str) -> Result<()>;
404
405 async fn get_bucket_cors(&self, bucket: &str) -> Result<Vec<CorsRule>>;
407
408 async fn set_bucket_cors(&self, bucket: &str, rules: Vec<CorsRule>) -> Result<()>;
410
411 async fn delete_bucket_cors(&self, bucket: &str) -> Result<()>;
413
414 async fn select_object_content(
416 &self,
417 path: &RemotePath,
418 options: &SelectOptions,
419 writer: &mut (dyn AsyncWrite + Send + Unpin),
420 ) -> Result<()>;
421 }
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[test]
432 fn test_object_info_file() {
433 let info = ObjectInfo::file("test.txt", 1024);
434 assert_eq!(info.key, "test.txt");
435 assert_eq!(info.size_bytes, Some(1024));
436 assert!(!info.is_dir);
437 }
438
439 #[test]
440 fn test_object_info_dir() {
441 let info = ObjectInfo::dir("path/to/dir/");
442 assert_eq!(info.key, "path/to/dir/");
443 assert!(info.is_dir);
444 assert!(info.size_bytes.is_none());
445 }
446
447 #[test]
448 fn test_object_info_bucket() {
449 let info = ObjectInfo::bucket("my-bucket");
450 assert_eq!(info.key, "my-bucket");
451 assert!(info.is_dir);
452 }
453
454 #[test]
455 fn test_object_info_metadata_default_none() {
456 let info = ObjectInfo::file("test.txt", 1024);
457 assert!(info.metadata.is_none());
458 }
459
460 #[test]
461 fn test_object_info_metadata_set() {
462 let mut info = ObjectInfo::file("test.txt", 1024);
463 let mut meta = HashMap::new();
464 meta.insert("content-disposition".to_string(), "attachment".to_string());
465 meta.insert("custom-key".to_string(), "custom-value".to_string());
466 info.metadata = Some(meta);
467
468 let metadata = info.metadata.as_ref().expect("metadata should be Some");
469 assert_eq!(metadata.len(), 2);
470 assert_eq!(metadata.get("content-disposition").unwrap(), "attachment");
471 assert_eq!(metadata.get("custom-key").unwrap(), "custom-value");
472 }
473}