Skip to main content

dog_blob/
store.rs

1use async_trait::async_trait;
2use chrono::Datelike;
3use crate::{BlobResult, ByteRange, ByteStream, UploadId};
4
5/// Information about a stored blob
6#[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/// Rich metadata for blobs
18#[derive(Debug, Clone, Default)]
19pub struct BlobMetadata {
20    // Audio metadata
21    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>, // seconds
27    pub bitrate: Option<u32>,
28    
29    // Visual metadata
30    pub thumbnail_url: Option<String>,
31    pub album_art_url: Option<String>,
32    
33    // Location metadata
34    pub latitude: Option<f64>,
35    pub longitude: Option<f64>,
36    pub location_name: Option<String>,
37    
38    // Technical metadata
39    pub mime_type: Option<String>,
40    pub encoding: Option<String>,
41    pub sample_rate: Option<u32>,
42    pub channels: Option<u32>,
43    
44    // Custom attributes
45    pub custom: std::collections::HashMap<String, String>,
46}
47
48/// Core blob storage operations - must be implemented by all storage backends
49#[async_trait]
50pub trait BlobStore: Send + Sync {
51    /// Enable downcasting to concrete types
52    fn as_any(&self) -> &dyn std::any::Any;
53    /// Store a blob from a stream
54    async fn put(
55        &self,
56        key: &str,
57        content_type: Option<&str>,
58        stream: ByteStream,
59    ) -> BlobResult<PutResult>;
60
61    /// Store a blob from a stream with metadata
62    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        // Default implementation falls back to regular put
70        self.put(key, content_type, stream).await
71    }
72
73    /// Get a blob as a stream, optionally with range support
74    async fn get(
75        &self,
76        key: &str,
77        range: Option<ByteRange>,
78    ) -> BlobResult<GetResult>;
79
80    /// Get blob metadata without content
81    async fn head(&self, key: &str) -> BlobResult<ObjectHead>;
82
83    /// Delete a blob
84    async fn delete(&self, key: &str) -> BlobResult<()>;
85
86    /// List blobs with optional prefix filter
87    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    /// Get store capabilities
93    fn capabilities(&self) -> StoreCapabilities;
94}
95
96/// Optional multipart upload support
97#[async_trait]
98pub trait MultipartBlobStore: BlobStore {
99    /// Initialize a multipart upload
100    async fn init_multipart(
101        &self,
102        key: &str,
103        content_type: Option<&str>,
104    ) -> BlobResult<UploadId>;
105
106    /// Upload a part
107    async fn put_part(
108        &self,
109        upload_id: &UploadId,
110        part_number: u32,
111        stream: ByteStream,
112    ) -> BlobResult<PartETag>;
113
114    /// Complete multipart upload
115    async fn complete_multipart(
116        &self,
117        upload_id: &UploadId,
118        parts: Vec<CompletedPart>,
119    ) -> BlobResult<PutResult>;
120
121    /// Abort multipart upload
122    async fn abort_multipart(&self, upload_id: &UploadId) -> BlobResult<()>;
123}
124
125/// Optional signed URL support
126#[async_trait]
127pub trait SignedUrlBlobStore: BlobStore {
128    /// Generate a signed URL for reading
129    async fn sign_get(&self, key: &str, expires_in_secs: u64) -> BlobResult<String>;
130
131    /// Generate a signed URL for writing
132    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/// Result of a successful put operation
141#[derive(Debug, Clone)]
142pub struct PutResult {
143    pub etag: Option<String>,
144    pub size_bytes: u64,
145    pub checksum: Option<String>,
146}
147
148/// Result of a get operation
149pub 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/// Metadata about a blob
158#[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/// ETag for a multipart part
167#[derive(Debug, Clone)]
168pub struct PartETag {
169    pub part_number: u32,
170    pub etag: String,
171}
172
173/// Completed part for multipart upload
174#[derive(Debug, Clone)]
175pub struct CompletedPart {
176    pub part_number: u32,
177    pub etag: String,
178}
179
180/// Resolved range information
181#[derive(Debug, Clone)]
182pub struct ResolvedRange {
183    pub start: u64,
184    pub end: u64,
185    pub total_size: u64,
186}
187
188/// Store capabilities
189#[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
227/// Strategy for generating blob keys
228pub trait BlobKeyStrategy: Send + Sync {
229    /// Generate a key for a blob
230    fn object_key(&self, tenant_id: &str, blob_id: &str, hints: &std::collections::BTreeMap<String, String>) -> String;
231
232    /// Generate a key for a derived asset
233    fn derived_key(&self, original_key: &str, kind: &str) -> String;
234
235    /// Generate a staging key for multipart uploads
236    fn staging_key(&self, tenant_id: &str, upload_id: &str, part_number: u32) -> String;
237}
238
239/// Default key strategy: tenant/year/month/blob_id
240#[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}