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::encryption::{BucketEncryption, ObjectEncryptionRequest};
15use crate::error::Result;
16use crate::lifecycle::LifecycleRule;
17use crate::path::RemotePath;
18use crate::replication::ReplicationConfiguration;
19use crate::select::SelectOptions;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ObjectVersion {
24 pub key: String,
26
27 pub version_id: String,
29
30 pub is_latest: bool,
32
33 pub is_delete_marker: bool,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub last_modified: Option<Timestamp>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub size_bytes: Option<i64>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub etag: Option<String>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ObjectVersionListResult {
52 pub items: Vec<ObjectVersion>,
54
55 pub truncated: bool,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub continuation_token: Option<String>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub version_id_marker: Option<String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ObjectInfo {
70 pub key: String,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub size_bytes: Option<i64>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub size_human: Option<String>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub last_modified: Option<Timestamp>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub etag: Option<String>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub storage_class: Option<String>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub content_type: Option<String>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub metadata: Option<HashMap<String, String>>,
100
101 pub is_dir: bool,
103}
104
105impl ObjectInfo {
106 pub fn file(key: impl Into<String>, size: i64) -> Self {
108 Self {
109 key: key.into(),
110 size_bytes: Some(size),
111 size_human: Some(humansize::format_size(size as u64, humansize::BINARY)),
112 last_modified: None,
113 etag: None,
114 storage_class: None,
115 content_type: None,
116 metadata: None,
117 is_dir: false,
118 }
119 }
120
121 pub fn dir(key: impl Into<String>) -> Self {
123 Self {
124 key: key.into(),
125 size_bytes: None,
126 size_human: None,
127 last_modified: None,
128 etag: None,
129 storage_class: None,
130 content_type: None,
131 metadata: None,
132 is_dir: true,
133 }
134 }
135
136 pub fn bucket(name: impl Into<String>) -> Self {
138 Self {
139 key: name.into(),
140 size_bytes: None,
141 size_human: None,
142 last_modified: None,
143 etag: None,
144 storage_class: None,
145 content_type: None,
146 metadata: None,
147 is_dir: true,
148 }
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct ListResult {
155 pub items: Vec<ObjectInfo>,
157
158 pub truncated: bool,
160
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub continuation_token: Option<String>,
164}
165
166#[derive(Debug, Clone, Default)]
168pub struct ListOptions {
169 pub max_keys: Option<i32>,
171
172 pub delimiter: Option<String>,
174
175 pub prefix: Option<String>,
177
178 pub continuation_token: Option<String>,
180
181 pub recursive: bool,
183}
184
185#[derive(Debug, Clone, Default)]
187pub struct Capabilities {
188 pub versioning: bool,
190
191 pub object_lock: bool,
193
194 pub tagging: bool,
196
197 pub anonymous: bool,
199
200 pub select: bool,
205
206 pub notifications: bool,
208
209 pub lifecycle: bool,
211
212 pub replication: bool,
214
215 pub cors: bool,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(rename_all = "lowercase")]
222pub enum NotificationTarget {
223 Queue,
225 Topic,
227 Lambda,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub struct BucketNotification {
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub id: Option<String>,
237 pub target: NotificationTarget,
239 pub arn: String,
241 pub events: Vec<String>,
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub prefix: Option<String>,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub suffix: Option<String>,
249}
250
251#[async_trait]
255pub trait ObjectStore: Send + Sync {
256 async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
258
259 async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
261
262 async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
264
265 async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
267
268 async fn create_bucket(&self, bucket: &str) -> Result<()>;
270
271 async fn delete_bucket(&self, bucket: &str) -> Result<()>;
273
274 async fn capabilities(&self) -> Result<Capabilities>;
276
277 async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
279
280 async fn put_object(
282 &self,
283 path: &RemotePath,
284 data: Vec<u8>,
285 content_type: Option<&str>,
286 encryption: Option<&ObjectEncryptionRequest>,
287 ) -> Result<ObjectInfo>;
288
289 async fn delete_object(&self, path: &RemotePath) -> Result<()>;
291
292 async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
294
295 async fn copy_object(
297 &self,
298 src: &RemotePath,
299 dst: &RemotePath,
300 encryption: Option<&ObjectEncryptionRequest>,
301 ) -> Result<ObjectInfo>;
302
303 async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
305
306 async fn presign_put(
308 &self,
309 path: &RemotePath,
310 expires_secs: u64,
311 content_type: Option<&str>,
312 ) -> Result<String>;
313
314 async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
318
319 async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
321
322 async fn get_bucket_encryption(&self, bucket: &str) -> Result<Option<BucketEncryption>>;
324
325 async fn set_bucket_encryption(&self, bucket: &str, encryption: BucketEncryption)
327 -> Result<()>;
328
329 async fn delete_bucket_encryption(&self, bucket: &str) -> Result<()>;
331
332 async fn list_object_versions(
334 &self,
335 path: &RemotePath,
336 max_keys: Option<i32>,
337 ) -> Result<Vec<ObjectVersion>>;
338
339 async fn get_object_tags(
341 &self,
342 path: &RemotePath,
343 ) -> Result<std::collections::HashMap<String, String>>;
344
345 async fn get_bucket_tags(
347 &self,
348 bucket: &str,
349 ) -> Result<std::collections::HashMap<String, String>>;
350
351 async fn set_object_tags(
353 &self,
354 path: &RemotePath,
355 tags: std::collections::HashMap<String, String>,
356 ) -> Result<()>;
357
358 async fn set_bucket_tags(
360 &self,
361 bucket: &str,
362 tags: std::collections::HashMap<String, String>,
363 ) -> Result<()>;
364
365 async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
367
368 async fn delete_bucket_tags(&self, bucket: &str) -> Result<()>;
370
371 async fn get_bucket_policy(&self, bucket: &str) -> Result<Option<String>>;
373
374 async fn set_bucket_policy(&self, bucket: &str, policy: &str) -> Result<()>;
376
377 async fn delete_bucket_policy(&self, bucket: &str) -> Result<()>;
379
380 async fn get_bucket_notifications(&self, bucket: &str) -> Result<Vec<BucketNotification>>;
382
383 async fn set_bucket_notifications(
385 &self,
386 bucket: &str,
387 notifications: Vec<BucketNotification>,
388 ) -> Result<()>;
389
390 async fn get_bucket_lifecycle(&self, bucket: &str) -> Result<Vec<LifecycleRule>>;
394
395 async fn set_bucket_lifecycle(&self, bucket: &str, rules: Vec<LifecycleRule>) -> Result<()>;
397
398 async fn delete_bucket_lifecycle(&self, bucket: &str) -> Result<()>;
400
401 async fn restore_object(&self, path: &RemotePath, days: i32) -> Result<()>;
403
404 async fn get_bucket_replication(
408 &self,
409 bucket: &str,
410 ) -> Result<Option<ReplicationConfiguration>>;
411
412 async fn set_bucket_replication(
414 &self,
415 bucket: &str,
416 config: ReplicationConfiguration,
417 ) -> Result<()>;
418
419 async fn delete_bucket_replication(&self, bucket: &str) -> Result<()>;
421
422 async fn get_bucket_cors(&self, bucket: &str) -> Result<Vec<CorsRule>>;
424
425 async fn set_bucket_cors(&self, bucket: &str, rules: Vec<CorsRule>) -> Result<()>;
427
428 async fn delete_bucket_cors(&self, bucket: &str) -> Result<()>;
430
431 async fn select_object_content(
433 &self,
434 path: &RemotePath,
435 options: &SelectOptions,
436 writer: &mut (dyn AsyncWrite + Send + Unpin),
437 ) -> Result<()>;
438 }
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn test_object_info_file() {
450 let info = ObjectInfo::file("test.txt", 1024);
451 assert_eq!(info.key, "test.txt");
452 assert_eq!(info.size_bytes, Some(1024));
453 assert!(!info.is_dir);
454 }
455
456 #[test]
457 fn test_object_info_dir() {
458 let info = ObjectInfo::dir("path/to/dir/");
459 assert_eq!(info.key, "path/to/dir/");
460 assert!(info.is_dir);
461 assert!(info.size_bytes.is_none());
462 }
463
464 #[test]
465 fn test_object_info_bucket() {
466 let info = ObjectInfo::bucket("my-bucket");
467 assert_eq!(info.key, "my-bucket");
468 assert!(info.is_dir);
469 }
470
471 #[test]
472 fn test_object_info_metadata_default_none() {
473 let info = ObjectInfo::file("test.txt", 1024);
474 assert!(info.metadata.is_none());
475 }
476
477 #[test]
478 fn test_object_info_metadata_set() {
479 let mut info = ObjectInfo::file("test.txt", 1024);
480 let mut meta = HashMap::new();
481 meta.insert("content-disposition".to_string(), "attachment".to_string());
482 meta.insert("custom-key".to_string(), "custom-value".to_string());
483 info.metadata = Some(meta);
484
485 let metadata = info.metadata.as_ref().expect("metadata should be Some");
486 assert_eq!(metadata.len(), 2);
487 assert_eq!(metadata.get("content-disposition").unwrap(), "attachment");
488 assert_eq!(metadata.get("custom-key").unwrap(), "custom-value");
489 }
490}