1use std::collections::HashMap;
7
8use async_trait::async_trait;
9use jiff::Timestamp;
10use serde::{Deserialize, Serialize};
11
12use crate::cors::CorsRule;
13use crate::error::Result;
14use crate::lifecycle::LifecycleRule;
15use crate::path::RemotePath;
16use crate::replication::ReplicationConfiguration;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ObjectVersion {
21 pub key: String,
23
24 pub version_id: String,
26
27 pub is_latest: bool,
29
30 pub is_delete_marker: bool,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub last_modified: Option<Timestamp>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub size_bytes: Option<i64>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub etag: Option<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ObjectInfo {
49 pub key: String,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub size_bytes: Option<i64>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub size_human: Option<String>,
59
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub last_modified: Option<Timestamp>,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub etag: Option<String>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub storage_class: Option<String>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub content_type: Option<String>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub metadata: Option<HashMap<String, String>>,
79
80 pub is_dir: bool,
82}
83
84impl ObjectInfo {
85 pub fn file(key: impl Into<String>, size: i64) -> Self {
87 Self {
88 key: key.into(),
89 size_bytes: Some(size),
90 size_human: Some(humansize::format_size(size as u64, humansize::BINARY)),
91 last_modified: None,
92 etag: None,
93 storage_class: None,
94 content_type: None,
95 metadata: None,
96 is_dir: false,
97 }
98 }
99
100 pub fn dir(key: impl Into<String>) -> Self {
102 Self {
103 key: key.into(),
104 size_bytes: None,
105 size_human: None,
106 last_modified: None,
107 etag: None,
108 storage_class: None,
109 content_type: None,
110 metadata: None,
111 is_dir: true,
112 }
113 }
114
115 pub fn bucket(name: impl Into<String>) -> Self {
117 Self {
118 key: name.into(),
119 size_bytes: None,
120 size_human: None,
121 last_modified: None,
122 etag: None,
123 storage_class: None,
124 content_type: None,
125 metadata: None,
126 is_dir: true,
127 }
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ListResult {
134 pub items: Vec<ObjectInfo>,
136
137 pub truncated: bool,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub continuation_token: Option<String>,
143}
144
145#[derive(Debug, Clone, Default)]
147pub struct ListOptions {
148 pub max_keys: Option<i32>,
150
151 pub delimiter: Option<String>,
153
154 pub prefix: Option<String>,
156
157 pub continuation_token: Option<String>,
159
160 pub recursive: bool,
162}
163
164#[derive(Debug, Clone, Default)]
166pub struct Capabilities {
167 pub versioning: bool,
169
170 pub object_lock: bool,
172
173 pub tagging: bool,
175
176 pub anonymous: bool,
178
179 pub select: bool,
181
182 pub notifications: bool,
184
185 pub lifecycle: bool,
187
188 pub replication: bool,
190
191 pub cors: bool,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
197#[serde(rename_all = "lowercase")]
198pub enum NotificationTarget {
199 Queue,
201 Topic,
203 Lambda,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209pub struct BucketNotification {
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub id: Option<String>,
213 pub target: NotificationTarget,
215 pub arn: String,
217 pub events: Vec<String>,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub prefix: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub suffix: Option<String>,
225}
226
227#[async_trait]
231pub trait ObjectStore: Send + Sync {
232 async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
234
235 async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
237
238 async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
240
241 async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
243
244 async fn create_bucket(&self, bucket: &str) -> Result<()>;
246
247 async fn delete_bucket(&self, bucket: &str) -> Result<()>;
249
250 async fn capabilities(&self) -> Result<Capabilities>;
252
253 async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
255
256 async fn put_object(
258 &self,
259 path: &RemotePath,
260 data: Vec<u8>,
261 content_type: Option<&str>,
262 ) -> Result<ObjectInfo>;
263
264 async fn delete_object(&self, path: &RemotePath) -> Result<()>;
266
267 async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
269
270 async fn copy_object(&self, src: &RemotePath, dst: &RemotePath) -> Result<ObjectInfo>;
272
273 async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
275
276 async fn presign_put(
278 &self,
279 path: &RemotePath,
280 expires_secs: u64,
281 content_type: Option<&str>,
282 ) -> Result<String>;
283
284 async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
288
289 async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
291
292 async fn list_object_versions(
294 &self,
295 path: &RemotePath,
296 max_keys: Option<i32>,
297 ) -> Result<Vec<ObjectVersion>>;
298
299 async fn get_object_tags(
301 &self,
302 path: &RemotePath,
303 ) -> Result<std::collections::HashMap<String, String>>;
304
305 async fn get_bucket_tags(
307 &self,
308 bucket: &str,
309 ) -> Result<std::collections::HashMap<String, String>>;
310
311 async fn set_object_tags(
313 &self,
314 path: &RemotePath,
315 tags: std::collections::HashMap<String, String>,
316 ) -> Result<()>;
317
318 async fn set_bucket_tags(
320 &self,
321 bucket: &str,
322 tags: std::collections::HashMap<String, String>,
323 ) -> Result<()>;
324
325 async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
327
328 async fn delete_bucket_tags(&self, bucket: &str) -> Result<()>;
330
331 async fn get_bucket_policy(&self, bucket: &str) -> Result<Option<String>>;
333
334 async fn set_bucket_policy(&self, bucket: &str, policy: &str) -> Result<()>;
336
337 async fn delete_bucket_policy(&self, bucket: &str) -> Result<()>;
339
340 async fn get_bucket_notifications(&self, bucket: &str) -> Result<Vec<BucketNotification>>;
342
343 async fn set_bucket_notifications(
345 &self,
346 bucket: &str,
347 notifications: Vec<BucketNotification>,
348 ) -> Result<()>;
349
350 async fn get_bucket_lifecycle(&self, bucket: &str) -> Result<Vec<LifecycleRule>>;
354
355 async fn set_bucket_lifecycle(&self, bucket: &str, rules: Vec<LifecycleRule>) -> Result<()>;
357
358 async fn delete_bucket_lifecycle(&self, bucket: &str) -> Result<()>;
360
361 async fn restore_object(&self, path: &RemotePath, days: i32) -> Result<()>;
363
364 async fn get_bucket_replication(
368 &self,
369 bucket: &str,
370 ) -> Result<Option<ReplicationConfiguration>>;
371
372 async fn set_bucket_replication(
374 &self,
375 bucket: &str,
376 config: ReplicationConfiguration,
377 ) -> Result<()>;
378
379 async fn delete_bucket_replication(&self, bucket: &str) -> Result<()>;
381
382 async fn get_bucket_cors(&self, bucket: &str) -> Result<Vec<CorsRule>>;
384
385 async fn set_bucket_cors(&self, bucket: &str, rules: Vec<CorsRule>) -> Result<()>;
387
388 async fn delete_bucket_cors(&self, bucket: &str) -> Result<()>;
390 }
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn test_object_info_file() {
402 let info = ObjectInfo::file("test.txt", 1024);
403 assert_eq!(info.key, "test.txt");
404 assert_eq!(info.size_bytes, Some(1024));
405 assert!(!info.is_dir);
406 }
407
408 #[test]
409 fn test_object_info_dir() {
410 let info = ObjectInfo::dir("path/to/dir/");
411 assert_eq!(info.key, "path/to/dir/");
412 assert!(info.is_dir);
413 assert!(info.size_bytes.is_none());
414 }
415
416 #[test]
417 fn test_object_info_bucket() {
418 let info = ObjectInfo::bucket("my-bucket");
419 assert_eq!(info.key, "my-bucket");
420 assert!(info.is_dir);
421 }
422
423 #[test]
424 fn test_object_info_metadata_default_none() {
425 let info = ObjectInfo::file("test.txt", 1024);
426 assert!(info.metadata.is_none());
427 }
428
429 #[test]
430 fn test_object_info_metadata_set() {
431 let mut info = ObjectInfo::file("test.txt", 1024);
432 let mut meta = HashMap::new();
433 meta.insert("content-disposition".to_string(), "attachment".to_string());
434 meta.insert("custom-key".to_string(), "custom-value".to_string());
435 info.metadata = Some(meta);
436
437 let metadata = info.metadata.as_ref().expect("metadata should be Some");
438 assert_eq!(metadata.len(), 2);
439 assert_eq!(metadata.get("content-disposition").unwrap(), "attachment");
440 assert_eq!(metadata.get("custom-key").unwrap(), "custom-value");
441 }
442}