1use async_trait::async_trait;
2use chrono::Datelike;
3use crate::{BlobResult, ByteRange, ByteStream, UploadId};
4
5#[derive(Debug, Clone)]
7pub struct BlobInfo {
8 pub key: String,
9 pub size_bytes: u64,
10 pub content_type: Option<String>,
11 pub filename: Option<String>,
12 pub etag: Option<String>,
13 pub last_modified: Option<i64>,
14 pub metadata: BlobMetadata,
15}
16
17#[derive(Debug, Clone, Default)]
19pub struct BlobMetadata {
20 pub title: Option<String>,
22 pub artist: Option<String>,
23 pub album: Option<String>,
24 pub genre: Option<String>,
25 pub year: Option<u32>,
26 pub duration: Option<u32>, pub bitrate: Option<u32>,
28
29 pub thumbnail_url: Option<String>,
31 pub album_art_url: Option<String>,
32
33 pub latitude: Option<f64>,
35 pub longitude: Option<f64>,
36 pub location_name: Option<String>,
37
38 pub mime_type: Option<String>,
40 pub encoding: Option<String>,
41 pub sample_rate: Option<u32>,
42 pub channels: Option<u32>,
43
44 pub custom: std::collections::HashMap<String, String>,
46}
47
48#[async_trait]
50pub trait BlobStore: Send + Sync {
51 fn as_any(&self) -> &dyn std::any::Any;
53 async fn put(
55 &self,
56 key: &str,
57 content_type: Option<&str>,
58 stream: ByteStream,
59 ) -> BlobResult<PutResult>;
60
61 async fn put_with_metadata(
63 &self,
64 key: &str,
65 content_type: Option<&str>,
66 _filename: Option<&str>,
67 stream: ByteStream,
68 ) -> BlobResult<PutResult> {
69 self.put(key, content_type, stream).await
71 }
72
73 async fn get(
75 &self,
76 key: &str,
77 range: Option<ByteRange>,
78 ) -> BlobResult<GetResult>;
79
80 async fn head(&self, key: &str) -> BlobResult<ObjectHead>;
82
83 async fn delete(&self, key: &str) -> BlobResult<()>;
85
86 async fn list(&self, prefix: Option<&str>, limit: Option<usize>) -> BlobResult<Vec<BlobInfo>> {
88 let _ = (prefix, limit);
89 Err(crate::BlobError::Unsupported)
90 }
91
92 fn capabilities(&self) -> StoreCapabilities;
94}
95
96#[async_trait]
98pub trait MultipartBlobStore: BlobStore {
99 async fn init_multipart(
101 &self,
102 key: &str,
103 content_type: Option<&str>,
104 ) -> BlobResult<UploadId>;
105
106 async fn put_part(
108 &self,
109 upload_id: &UploadId,
110 part_number: u32,
111 stream: ByteStream,
112 ) -> BlobResult<PartETag>;
113
114 async fn complete_multipart(
116 &self,
117 upload_id: &UploadId,
118 parts: Vec<CompletedPart>,
119 ) -> BlobResult<PutResult>;
120
121 async fn abort_multipart(&self, upload_id: &UploadId) -> BlobResult<()>;
123}
124
125#[async_trait]
127pub trait SignedUrlBlobStore: BlobStore {
128 async fn sign_get(&self, key: &str, expires_in_secs: u64) -> BlobResult<String>;
130
131 async fn sign_put(
133 &self,
134 key: &str,
135 content_type: Option<&str>,
136 expires_in_secs: u64,
137 ) -> BlobResult<String>;
138}
139
140#[derive(Debug, Clone)]
142pub struct PutResult {
143 pub etag: Option<String>,
144 pub size_bytes: u64,
145 pub checksum: Option<String>,
146}
147
148pub struct GetResult {
150 pub stream: ByteStream,
151 pub size_bytes: u64,
152 pub content_type: Option<String>,
153 pub etag: Option<String>,
154 pub resolved_range: Option<ResolvedRange>,
155}
156
157#[derive(Debug, Clone)]
159pub struct ObjectHead {
160 pub size_bytes: u64,
161 pub content_type: Option<String>,
162 pub etag: Option<String>,
163 pub last_modified: Option<i64>,
164}
165
166#[derive(Debug, Clone)]
168pub struct PartETag {
169 pub part_number: u32,
170 pub etag: String,
171}
172
173#[derive(Debug, Clone)]
175pub struct CompletedPart {
176 pub part_number: u32,
177 pub etag: String,
178}
179
180#[derive(Debug, Clone)]
182pub struct ResolvedRange {
183 pub start: u64,
184 pub end: u64,
185 pub total_size: u64,
186}
187
188#[derive(Debug, Clone, Default)]
190pub struct StoreCapabilities {
191 pub supports_range: bool,
192 pub supports_multipart: bool,
193 pub supports_signed_urls: bool,
194 pub max_part_size: Option<u64>,
195 pub min_part_size: Option<u64>,
196}
197
198impl StoreCapabilities {
199 pub fn basic() -> Self {
200 Self {
201 supports_range: false,
202 supports_multipart: false,
203 supports_signed_urls: false,
204 max_part_size: None,
205 min_part_size: None,
206 }
207 }
208
209 pub fn with_range(mut self) -> Self {
210 self.supports_range = true;
211 self
212 }
213
214 pub fn with_multipart(mut self, min_size: Option<u64>, max_size: Option<u64>) -> Self {
215 self.supports_multipart = true;
216 self.min_part_size = min_size;
217 self.max_part_size = max_size;
218 self
219 }
220
221 pub fn with_signed_urls(mut self) -> Self {
222 self.supports_signed_urls = true;
223 self
224 }
225}
226
227pub trait BlobKeyStrategy: Send + Sync {
229 fn object_key(&self, tenant_id: &str, blob_id: &str, hints: &std::collections::BTreeMap<String, String>) -> String;
231
232 fn derived_key(&self, original_key: &str, kind: &str) -> String;
234
235 fn staging_key(&self, tenant_id: &str, upload_id: &str, part_number: u32) -> String;
237}
238
239#[derive(Debug, Clone)]
241pub struct DefaultKeyStrategy;
242
243impl BlobKeyStrategy for DefaultKeyStrategy {
244 fn object_key(&self, tenant_id: &str, blob_id: &str, _hints: &std::collections::BTreeMap<String, String>) -> String {
245 let now = std::time::SystemTime::now()
246 .duration_since(std::time::UNIX_EPOCH)
247 .unwrap_or_default()
248 .as_secs();
249
250 let dt = chrono::DateTime::from_timestamp(now as i64, 0)
251 .unwrap_or_else(|| chrono::Utc::now());
252
253 format!("{}/{:04}/{:02}/{}",
254 tenant_id,
255 dt.year(),
256 dt.month(),
257 blob_id
258 )
259 }
260
261 fn derived_key(&self, original_key: &str, kind: &str) -> String {
262 format!("{}.{}", original_key, kind)
263 }
264
265 fn staging_key(&self, tenant_id: &str, upload_id: &str, part_number: u32) -> String {
266 format!("__uploads/{}/{}/part-{:06}", tenant_id, upload_id, part_number)
267 }
268}