1use async_trait::async_trait;
7use jiff::Timestamp;
8use serde::{Deserialize, Serialize};
9
10use crate::error::Result;
11use crate::path::RemotePath;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ObjectVersion {
16 pub key: String,
18
19 pub version_id: String,
21
22 pub is_latest: bool,
24
25 pub is_delete_marker: bool,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub last_modified: Option<Timestamp>,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub size_bytes: Option<i64>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub etag: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ObjectInfo {
44 pub key: String,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub size_bytes: Option<i64>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub size_human: Option<String>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub last_modified: Option<Timestamp>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub etag: Option<String>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub storage_class: Option<String>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub content_type: Option<String>,
70
71 pub is_dir: bool,
73}
74
75impl ObjectInfo {
76 pub fn file(key: impl Into<String>, size: i64) -> Self {
78 Self {
79 key: key.into(),
80 size_bytes: Some(size),
81 size_human: Some(humansize::format_size(size as u64, humansize::BINARY)),
82 last_modified: None,
83 etag: None,
84 storage_class: None,
85 content_type: None,
86 is_dir: false,
87 }
88 }
89
90 pub fn dir(key: impl Into<String>) -> Self {
92 Self {
93 key: key.into(),
94 size_bytes: None,
95 size_human: None,
96 last_modified: None,
97 etag: None,
98 storage_class: None,
99 content_type: None,
100 is_dir: true,
101 }
102 }
103
104 pub fn bucket(name: impl Into<String>) -> Self {
106 Self {
107 key: name.into(),
108 size_bytes: None,
109 size_human: None,
110 last_modified: None,
111 etag: None,
112 storage_class: None,
113 content_type: None,
114 is_dir: true,
115 }
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ListResult {
122 pub items: Vec<ObjectInfo>,
124
125 pub truncated: bool,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub continuation_token: Option<String>,
131}
132
133#[derive(Debug, Clone, Default)]
135pub struct ListOptions {
136 pub max_keys: Option<i32>,
138
139 pub delimiter: Option<String>,
141
142 pub prefix: Option<String>,
144
145 pub continuation_token: Option<String>,
147
148 pub recursive: bool,
150}
151
152#[derive(Debug, Clone, Default)]
154pub struct Capabilities {
155 pub versioning: bool,
157
158 pub object_lock: bool,
160
161 pub tagging: bool,
163
164 pub select: bool,
166
167 pub notifications: bool,
169}
170
171#[async_trait]
175pub trait ObjectStore: Send + Sync {
176 async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
178
179 async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
181
182 async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
184
185 async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
187
188 async fn create_bucket(&self, bucket: &str) -> Result<()>;
190
191 async fn delete_bucket(&self, bucket: &str) -> Result<()>;
193
194 async fn capabilities(&self) -> Result<Capabilities>;
196
197 async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
199
200 async fn put_object(
202 &self,
203 path: &RemotePath,
204 data: Vec<u8>,
205 content_type: Option<&str>,
206 ) -> Result<ObjectInfo>;
207
208 async fn delete_object(&self, path: &RemotePath) -> Result<()>;
210
211 async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
213
214 async fn copy_object(&self, src: &RemotePath, dst: &RemotePath) -> Result<ObjectInfo>;
216
217 async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
219
220 async fn presign_put(
222 &self,
223 path: &RemotePath,
224 expires_secs: u64,
225 content_type: Option<&str>,
226 ) -> Result<String>;
227
228 async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
232
233 async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
235
236 async fn list_object_versions(
238 &self,
239 path: &RemotePath,
240 max_keys: Option<i32>,
241 ) -> Result<Vec<ObjectVersion>>;
242
243 async fn get_object_tags(
245 &self,
246 path: &RemotePath,
247 ) -> Result<std::collections::HashMap<String, String>>;
248
249 async fn set_object_tags(
251 &self,
252 path: &RemotePath,
253 tags: std::collections::HashMap<String, String>,
254 ) -> Result<()>;
255
256 async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
258 }
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_object_info_file() {
270 let info = ObjectInfo::file("test.txt", 1024);
271 assert_eq!(info.key, "test.txt");
272 assert_eq!(info.size_bytes, Some(1024));
273 assert!(!info.is_dir);
274 }
275
276 #[test]
277 fn test_object_info_dir() {
278 let info = ObjectInfo::dir("path/to/dir/");
279 assert_eq!(info.key, "path/to/dir/");
280 assert!(info.is_dir);
281 assert!(info.size_bytes.is_none());
282 }
283
284 #[test]
285 fn test_object_info_bucket() {
286 let info = ObjectInfo::bucket("my-bucket");
287 assert_eq!(info.key, "my-bucket");
288 assert!(info.is_dir);
289 }
290}