1use std::collections::HashMap;
7
8use async_trait::async_trait;
9use jiff::Timestamp;
10use serde::{Deserialize, Serialize};
11
12use crate::error::Result;
13use crate::path::RemotePath;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ObjectVersion {
18 pub key: String,
20
21 pub version_id: String,
23
24 pub is_latest: bool,
26
27 pub is_delete_marker: bool,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub last_modified: Option<Timestamp>,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub size_bytes: Option<i64>,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub etag: Option<String>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ObjectInfo {
46 pub key: String,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub size_bytes: Option<i64>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub size_human: Option<String>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub last_modified: Option<Timestamp>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub etag: Option<String>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub storage_class: Option<String>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub content_type: Option<String>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub metadata: Option<HashMap<String, String>>,
76
77 pub is_dir: bool,
79}
80
81impl ObjectInfo {
82 pub fn file(key: impl Into<String>, size: i64) -> Self {
84 Self {
85 key: key.into(),
86 size_bytes: Some(size),
87 size_human: Some(humansize::format_size(size as u64, humansize::BINARY)),
88 last_modified: None,
89 etag: None,
90 storage_class: None,
91 content_type: None,
92 metadata: None,
93 is_dir: false,
94 }
95 }
96
97 pub fn dir(key: impl Into<String>) -> Self {
99 Self {
100 key: key.into(),
101 size_bytes: None,
102 size_human: None,
103 last_modified: None,
104 etag: None,
105 storage_class: None,
106 content_type: None,
107 metadata: None,
108 is_dir: true,
109 }
110 }
111
112 pub fn bucket(name: impl Into<String>) -> Self {
114 Self {
115 key: name.into(),
116 size_bytes: None,
117 size_human: None,
118 last_modified: None,
119 etag: None,
120 storage_class: None,
121 content_type: None,
122 metadata: None,
123 is_dir: true,
124 }
125 }
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ListResult {
131 pub items: Vec<ObjectInfo>,
133
134 pub truncated: bool,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub continuation_token: Option<String>,
140}
141
142#[derive(Debug, Clone, Default)]
144pub struct ListOptions {
145 pub max_keys: Option<i32>,
147
148 pub delimiter: Option<String>,
150
151 pub prefix: Option<String>,
153
154 pub continuation_token: Option<String>,
156
157 pub recursive: bool,
159}
160
161#[derive(Debug, Clone, Default)]
163pub struct Capabilities {
164 pub versioning: bool,
166
167 pub object_lock: bool,
169
170 pub tagging: bool,
172
173 pub anonymous: bool,
175
176 pub select: bool,
178
179 pub notifications: bool,
181}
182
183#[async_trait]
187pub trait ObjectStore: Send + Sync {
188 async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
190
191 async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
193
194 async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
196
197 async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
199
200 async fn create_bucket(&self, bucket: &str) -> Result<()>;
202
203 async fn delete_bucket(&self, bucket: &str) -> Result<()>;
205
206 async fn capabilities(&self) -> Result<Capabilities>;
208
209 async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
211
212 async fn put_object(
214 &self,
215 path: &RemotePath,
216 data: Vec<u8>,
217 content_type: Option<&str>,
218 ) -> Result<ObjectInfo>;
219
220 async fn delete_object(&self, path: &RemotePath) -> Result<()>;
222
223 async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
225
226 async fn copy_object(&self, src: &RemotePath, dst: &RemotePath) -> Result<ObjectInfo>;
228
229 async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
231
232 async fn presign_put(
234 &self,
235 path: &RemotePath,
236 expires_secs: u64,
237 content_type: Option<&str>,
238 ) -> Result<String>;
239
240 async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
244
245 async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
247
248 async fn list_object_versions(
250 &self,
251 path: &RemotePath,
252 max_keys: Option<i32>,
253 ) -> Result<Vec<ObjectVersion>>;
254
255 async fn get_object_tags(
257 &self,
258 path: &RemotePath,
259 ) -> Result<std::collections::HashMap<String, String>>;
260
261 async fn get_bucket_tags(
263 &self,
264 bucket: &str,
265 ) -> Result<std::collections::HashMap<String, String>>;
266
267 async fn set_object_tags(
269 &self,
270 path: &RemotePath,
271 tags: std::collections::HashMap<String, String>,
272 ) -> Result<()>;
273
274 async fn set_bucket_tags(
276 &self,
277 bucket: &str,
278 tags: std::collections::HashMap<String, String>,
279 ) -> Result<()>;
280
281 async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
283
284 async fn delete_bucket_tags(&self, bucket: &str) -> Result<()>;
286
287 async fn get_bucket_policy(&self, bucket: &str) -> Result<Option<String>>;
289
290 async fn set_bucket_policy(&self, bucket: &str, policy: &str) -> Result<()>;
292
293 async fn delete_bucket_policy(&self, bucket: &str) -> Result<()>;
295 }
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_object_info_file() {
307 let info = ObjectInfo::file("test.txt", 1024);
308 assert_eq!(info.key, "test.txt");
309 assert_eq!(info.size_bytes, Some(1024));
310 assert!(!info.is_dir);
311 }
312
313 #[test]
314 fn test_object_info_dir() {
315 let info = ObjectInfo::dir("path/to/dir/");
316 assert_eq!(info.key, "path/to/dir/");
317 assert!(info.is_dir);
318 assert!(info.size_bytes.is_none());
319 }
320
321 #[test]
322 fn test_object_info_bucket() {
323 let info = ObjectInfo::bucket("my-bucket");
324 assert_eq!(info.key, "my-bucket");
325 assert!(info.is_dir);
326 }
327
328 #[test]
329 fn test_object_info_metadata_default_none() {
330 let info = ObjectInfo::file("test.txt", 1024);
331 assert!(info.metadata.is_none());
332 }
333
334 #[test]
335 fn test_object_info_metadata_set() {
336 let mut info = ObjectInfo::file("test.txt", 1024);
337 let mut meta = HashMap::new();
338 meta.insert("content-disposition".to_string(), "attachment".to_string());
339 meta.insert("custom-key".to_string(), "custom-value".to_string());
340 info.metadata = Some(meta);
341
342 let metadata = info.metadata.as_ref().expect("metadata should be Some");
343 assert_eq!(metadata.len(), 2);
344 assert_eq!(metadata.get("content-disposition").unwrap(), "attachment");
345 assert_eq!(metadata.get("custom-key").unwrap(), "custom-value");
346 }
347}