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 ObjectInfo {
51 pub key: String,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub size_bytes: Option<i64>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub size_human: Option<String>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub last_modified: Option<Timestamp>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub etag: Option<String>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub storage_class: Option<String>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub content_type: Option<String>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub metadata: Option<HashMap<String, String>>,
81
82 pub is_dir: bool,
84}
85
86impl ObjectInfo {
87 pub fn file(key: impl Into<String>, size: i64) -> Self {
89 Self {
90 key: key.into(),
91 size_bytes: Some(size),
92 size_human: Some(humansize::format_size(size as u64, humansize::BINARY)),
93 last_modified: None,
94 etag: None,
95 storage_class: None,
96 content_type: None,
97 metadata: None,
98 is_dir: false,
99 }
100 }
101
102 pub fn dir(key: impl Into<String>) -> Self {
104 Self {
105 key: key.into(),
106 size_bytes: None,
107 size_human: None,
108 last_modified: None,
109 etag: None,
110 storage_class: None,
111 content_type: None,
112 metadata: None,
113 is_dir: true,
114 }
115 }
116
117 pub fn bucket(name: impl Into<String>) -> Self {
119 Self {
120 key: name.into(),
121 size_bytes: None,
122 size_human: None,
123 last_modified: None,
124 etag: None,
125 storage_class: None,
126 content_type: None,
127 metadata: None,
128 is_dir: true,
129 }
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ListResult {
136 pub items: Vec<ObjectInfo>,
138
139 pub truncated: bool,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub continuation_token: Option<String>,
145}
146
147#[derive(Debug, Clone, Default)]
149pub struct ListOptions {
150 pub max_keys: Option<i32>,
152
153 pub delimiter: Option<String>,
155
156 pub prefix: Option<String>,
158
159 pub continuation_token: Option<String>,
161
162 pub recursive: bool,
164}
165
166#[derive(Debug, Clone, Default)]
168pub struct Capabilities {
169 pub versioning: bool,
171
172 pub object_lock: bool,
174
175 pub tagging: bool,
177
178 pub anonymous: bool,
180
181 pub select: bool,
186
187 pub notifications: bool,
189
190 pub lifecycle: bool,
192
193 pub replication: bool,
195
196 pub cors: bool,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(rename_all = "lowercase")]
203pub enum NotificationTarget {
204 Queue,
206 Topic,
208 Lambda,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct BucketNotification {
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub id: Option<String>,
218 pub target: NotificationTarget,
220 pub arn: String,
222 pub events: Vec<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub prefix: Option<String>,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub suffix: Option<String>,
230}
231
232#[async_trait]
236pub trait ObjectStore: Send + Sync {
237 async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
239
240 async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
242
243 async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
245
246 async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
248
249 async fn create_bucket(&self, bucket: &str) -> Result<()>;
251
252 async fn delete_bucket(&self, bucket: &str) -> Result<()>;
254
255 async fn capabilities(&self) -> Result<Capabilities>;
257
258 async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
260
261 async fn put_object(
263 &self,
264 path: &RemotePath,
265 data: Vec<u8>,
266 content_type: Option<&str>,
267 ) -> Result<ObjectInfo>;
268
269 async fn delete_object(&self, path: &RemotePath) -> Result<()>;
271
272 async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
274
275 async fn copy_object(&self, src: &RemotePath, dst: &RemotePath) -> Result<ObjectInfo>;
277
278 async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
280
281 async fn presign_put(
283 &self,
284 path: &RemotePath,
285 expires_secs: u64,
286 content_type: Option<&str>,
287 ) -> Result<String>;
288
289 async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
293
294 async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
296
297 async fn list_object_versions(
299 &self,
300 path: &RemotePath,
301 max_keys: Option<i32>,
302 ) -> Result<Vec<ObjectVersion>>;
303
304 async fn get_object_tags(
306 &self,
307 path: &RemotePath,
308 ) -> Result<std::collections::HashMap<String, String>>;
309
310 async fn get_bucket_tags(
312 &self,
313 bucket: &str,
314 ) -> Result<std::collections::HashMap<String, String>>;
315
316 async fn set_object_tags(
318 &self,
319 path: &RemotePath,
320 tags: std::collections::HashMap<String, String>,
321 ) -> Result<()>;
322
323 async fn set_bucket_tags(
325 &self,
326 bucket: &str,
327 tags: std::collections::HashMap<String, String>,
328 ) -> Result<()>;
329
330 async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
332
333 async fn delete_bucket_tags(&self, bucket: &str) -> Result<()>;
335
336 async fn get_bucket_policy(&self, bucket: &str) -> Result<Option<String>>;
338
339 async fn set_bucket_policy(&self, bucket: &str, policy: &str) -> Result<()>;
341
342 async fn delete_bucket_policy(&self, bucket: &str) -> Result<()>;
344
345 async fn get_bucket_notifications(&self, bucket: &str) -> Result<Vec<BucketNotification>>;
347
348 async fn set_bucket_notifications(
350 &self,
351 bucket: &str,
352 notifications: Vec<BucketNotification>,
353 ) -> Result<()>;
354
355 async fn get_bucket_lifecycle(&self, bucket: &str) -> Result<Vec<LifecycleRule>>;
359
360 async fn set_bucket_lifecycle(&self, bucket: &str, rules: Vec<LifecycleRule>) -> Result<()>;
362
363 async fn delete_bucket_lifecycle(&self, bucket: &str) -> Result<()>;
365
366 async fn restore_object(&self, path: &RemotePath, days: i32) -> Result<()>;
368
369 async fn get_bucket_replication(
373 &self,
374 bucket: &str,
375 ) -> Result<Option<ReplicationConfiguration>>;
376
377 async fn set_bucket_replication(
379 &self,
380 bucket: &str,
381 config: ReplicationConfiguration,
382 ) -> Result<()>;
383
384 async fn delete_bucket_replication(&self, bucket: &str) -> Result<()>;
386
387 async fn get_bucket_cors(&self, bucket: &str) -> Result<Vec<CorsRule>>;
389
390 async fn set_bucket_cors(&self, bucket: &str, rules: Vec<CorsRule>) -> Result<()>;
392
393 async fn delete_bucket_cors(&self, bucket: &str) -> Result<()>;
395
396 async fn select_object_content(
398 &self,
399 path: &RemotePath,
400 options: &SelectOptions,
401 writer: &mut (dyn AsyncWrite + Send + Unpin),
402 ) -> Result<()>;
403 }
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_object_info_file() {
415 let info = ObjectInfo::file("test.txt", 1024);
416 assert_eq!(info.key, "test.txt");
417 assert_eq!(info.size_bytes, Some(1024));
418 assert!(!info.is_dir);
419 }
420
421 #[test]
422 fn test_object_info_dir() {
423 let info = ObjectInfo::dir("path/to/dir/");
424 assert_eq!(info.key, "path/to/dir/");
425 assert!(info.is_dir);
426 assert!(info.size_bytes.is_none());
427 }
428
429 #[test]
430 fn test_object_info_bucket() {
431 let info = ObjectInfo::bucket("my-bucket");
432 assert_eq!(info.key, "my-bucket");
433 assert!(info.is_dir);
434 }
435
436 #[test]
437 fn test_object_info_metadata_default_none() {
438 let info = ObjectInfo::file("test.txt", 1024);
439 assert!(info.metadata.is_none());
440 }
441
442 #[test]
443 fn test_object_info_metadata_set() {
444 let mut info = ObjectInfo::file("test.txt", 1024);
445 let mut meta = HashMap::new();
446 meta.insert("content-disposition".to_string(), "attachment".to_string());
447 meta.insert("custom-key".to_string(), "custom-value".to_string());
448 info.metadata = Some(meta);
449
450 let metadata = info.metadata.as_ref().expect("metadata should be Some");
451 assert_eq!(metadata.len(), 2);
452 assert_eq!(metadata.get("content-disposition").unwrap(), "attachment");
453 assert_eq!(metadata.get("custom-key").unwrap(), "custom-value");
454 }
455}