mediagit_storage/lib.rs
1// MediaGit - Git for Media Files
2// Copyright (C) 2025 MediaGit Contributors
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published
6// by the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13
14#![allow(missing_docs)]
15//! Storage abstraction layer for MediaGit
16//!
17//! This crate provides a unified, asynchronous storage interface that supports multiple backends:
18//! - Local filesystem (via `mediagit-local-storage`)
19//! - AWS S3
20//! - Azure Blob Storage
21//! - Google Cloud Storage
22//! - MinIO / S3-compatible
23//! - Backblaze B2 / DigitalOcean Spaces
24//!
25//! # Architecture
26//!
27//! The `StorageBackend` trait defines a minimal but complete interface for object storage
28//! operations, allowing implementations to handle various storage systems transparently.
29//!
30//! ## Core Concepts
31//!
32//! - **Keys**: Unique identifiers for stored objects (strings, typically hierarchical like file paths)
33//! - **Objects**: Arbitrary binary data associated with a key
34//! - **Prefixes**: String prefixes used for listing and organization (similar to S3 object prefixes)
35//!
36//! # Features
37//!
38//! - **Async-first**: All operations are async using `tokio` for non-blocking I/O
39//! - **Thread-safe**: All implementations must be `Send + Sync` for safe concurrent use
40//! - **Debuggable**: All implementations must implement `Debug`
41//! - **Error handling**: Uses `anyhow::Result` for ergonomic error management
42//!
43//! # Examples
44//!
45//! Using the mock backend for testing:
46//!
47//! ```no_run
48//! use mediagit_storage::{StorageBackend, mock::MockBackend};
49//!
50//! #[tokio::main]
51//! async fn main() -> anyhow::Result<()> {
52//! // Create an in-memory backend for testing
53//! let storage = MockBackend::new();
54//!
55//! // Store data
56//! storage.put("documents/resume.pdf", b"PDF content").await?;
57//!
58//! // Retrieve data
59//! let data = storage.get("documents/resume.pdf").await?;
60//! assert_eq!(data, b"PDF content");
61//!
62//! // Check existence
63//! if storage.exists("documents/resume.pdf").await? {
64//! println!("File exists");
65//! }
66//!
67//! // List objects with prefix
68//! let documents = storage.list_objects("documents/").await?;
69//! println!("Found {} documents", documents.len());
70//!
71//! // Delete object
72//! storage.delete("documents/resume.pdf").await?;
73//!
74//! Ok(())
75//! }
76//! ```
77//!
78//! # Implementation Guide
79//!
80//! When implementing `StorageBackend`:
81//!
82//! 1. Use `#[async_trait]` macro on your impl block
83//! 2. Return `anyhow::Result<T>` for all operations
84//! 3. Ensure your type implements `Send + Sync + Debug`
85//! 4. Handle empty keys gracefully (typically return an error)
86//! 5. List operations should return sorted results for consistency
87//! 6. Deleting non-existent objects should succeed (idempotent)
88//!
89//! # Error Handling
90//!
91//! While the trait uses `anyhow::Result`, consider using the `StorageError` enum
92//! in `error.rs` for more structured error information:
93//!
94//! ```no_run
95//! use mediagit_storage::error::{StorageError, StorageResult};
96//!
97//! fn validate_key(key: &str) -> StorageResult<()> {
98//! if key.is_empty() {
99//! Err(StorageError::invalid_key("key cannot be empty"))
100//! } else {
101//! Ok(())
102//! }
103//! }
104//! ```
105
106#[cfg(feature = "azure")]
107pub mod azure;
108pub mod b2_spaces;
109pub mod cache;
110pub mod error;
111#[cfg(feature = "gcs")]
112pub mod gcs;
113pub mod local;
114pub mod minio;
115pub mod mock;
116pub mod s3;
117
118use async_trait::async_trait;
119use std::fmt::Debug;
120
121#[cfg(feature = "azure")]
122pub use azure::AzureBackend;
123pub use b2_spaces::B2SpacesBackend;
124pub use error::{StorageError, StorageResult};
125#[cfg(feature = "gcs")]
126pub use gcs::GcsBackend;
127pub use local::LocalBackend;
128pub use minio::MinIOBackend;
129pub use s3::S3Backend;
130
131/// Storage backend trait for object storage operations
132///
133/// This trait defines the minimal interface for object storage systems.
134/// Implementations must be async-safe, thread-safe, and handle errors gracefully.
135///
136/// # Safety Requirements
137///
138/// All implementations must:
139/// - Be `Send` to cross thread boundaries
140/// - Be `Sync` for safe concurrent access
141/// - Implement `Debug` for observability
142/// - Be thread-safe and support concurrent operations
143///
144/// # Error Handling
145///
146/// All operations return `anyhow::Result<T>` to allow flexible error context.
147/// Operations should return `Err` for:
148/// - `get`: Key doesn't exist (use "object not found" message)
149/// - `put`: Permission denied, quota exceeded, or I/O errors
150/// - `exists`: Typically only I/O or permission errors
151/// - `delete`: Typically succeeds even if object doesn't exist (idempotent)
152/// - `list_objects`: Permission denied or I/O errors
153///
154/// # Examples
155///
156/// See [`mock::MockBackend`] for a complete example implementation.
157///
158/// ```rust,no_run
159/// # use mediagit_storage::{StorageBackend, mock::MockBackend};
160/// #[tokio::main]
161/// async fn example() -> anyhow::Result<()> {
162/// let backend: Box<dyn StorageBackend> = Box::new(MockBackend::new());
163///
164/// backend.put("my_key", b"my_data").await?;
165/// let retrieved = backend.get("my_key").await?;
166/// assert_eq!(retrieved, b"my_data");
167///
168/// Ok(())
169/// }
170/// ```
171#[async_trait]
172pub trait StorageBackend: Send + Sync + Debug {
173 /// Retrieve an object by its key
174 ///
175 /// # Arguments
176 ///
177 /// * `key` - The object identifier (non-empty string)
178 ///
179 /// # Returns
180 ///
181 /// * `Ok(Vec<u8>)` - The object data
182 /// * `Err` - If the key doesn't exist or an I/O error occurs
183 ///
184 /// # Errors
185 ///
186 /// Returns an error if:
187 /// - The key doesn't exist (should use "object not found" in the error message)
188 /// - An I/O error occurs
189 /// - Permission is denied
190 /// - The key is empty
191 ///
192 /// # Examples
193 ///
194 /// ```rust,no_run
195 /// # use mediagit_storage::{StorageBackend, mock::MockBackend};
196 /// # #[tokio::main]
197 /// # async fn main() -> anyhow::Result<()> {
198 /// let storage = MockBackend::new();
199 /// storage.put("document.pdf", b"content").await?;
200 ///
201 /// let data = storage.get("document.pdf").await?;
202 /// assert_eq!(data, b"content");
203 /// # Ok(())
204 /// # }
205 /// ```
206 async fn get(&self, key: &str) -> anyhow::Result<Vec<u8>>;
207
208 /// Store an object with the given key
209 ///
210 /// This operation is idempotent: calling it multiple times with the same key
211 /// will overwrite previous data.
212 ///
213 /// # Arguments
214 ///
215 /// * `key` - The object identifier (non-empty string)
216 /// * `data` - The object content (can be empty)
217 ///
218 /// # Returns
219 ///
220 /// * `Ok(())` - The operation succeeded
221 /// * `Err` - If an I/O error occurs or permission is denied
222 ///
223 /// # Errors
224 ///
225 /// Returns an error if:
226 /// - An I/O error occurs
227 /// - Permission is denied
228 /// - Storage quota exceeded
229 /// - The key is empty
230 ///
231 /// # Examples
232 ///
233 /// ```rust,no_run
234 /// # use mediagit_storage::{StorageBackend, mock::MockBackend};
235 /// # #[tokio::main]
236 /// # async fn main() -> anyhow::Result<()> {
237 /// let storage = MockBackend::new();
238 ///
239 /// let data = vec![0x89, 0x50, 0x4E, 0x47]; // PNG magic bytes
240 /// storage.put("image.png", &data).await?;
241 /// # Ok(())
242 /// # }
243 /// ```
244 async fn put(&self, key: &str, data: &[u8]) -> anyhow::Result<()>;
245
246 /// Check if an object exists
247 ///
248 /// # Arguments
249 ///
250 /// * `key` - The object identifier (non-empty string)
251 ///
252 /// # Returns
253 ///
254 /// * `Ok(true)` - The object exists
255 /// * `Ok(false)` - The object doesn't exist
256 /// * `Err` - If an I/O error occurs or permission is denied
257 ///
258 /// # Errors
259 ///
260 /// Returns an error if:
261 /// - An I/O error occurs
262 /// - Permission is denied
263 /// - The key is empty
264 ///
265 /// # Examples
266 ///
267 /// ```rust,no_run
268 /// # use mediagit_storage::{StorageBackend, mock::MockBackend};
269 /// # #[tokio::main]
270 /// # async fn main() -> anyhow::Result<()> {
271 /// let storage = MockBackend::new();
272 /// storage.put("file.txt", b"content").await?;
273 ///
274 /// assert!(storage.exists("file.txt").await?);
275 /// assert!(!storage.exists("missing.txt").await?);
276 /// # Ok(())
277 /// # }
278 /// ```
279 async fn exists(&self, key: &str) -> anyhow::Result<bool>;
280
281 /// Delete an object
282 ///
283 /// This operation is idempotent: deleting a non-existent object should succeed.
284 ///
285 /// # Arguments
286 ///
287 /// * `key` - The object identifier (non-empty string)
288 ///
289 /// # Returns
290 ///
291 /// * `Ok(())` - The operation succeeded (whether the object existed or not)
292 /// * `Err` - If an I/O error occurs or permission is denied
293 ///
294 /// # Errors
295 ///
296 /// Returns an error if:
297 /// - An I/O error occurs
298 /// - Permission is denied
299 /// - The key is empty
300 ///
301 /// Note: Most implementations should return `Ok(())` for non-existent keys
302 /// to support idempotent deletion.
303 ///
304 /// # Examples
305 ///
306 /// ```rust,no_run
307 /// # use mediagit_storage::{StorageBackend, mock::MockBackend};
308 /// # #[tokio::main]
309 /// # async fn main() -> anyhow::Result<()> {
310 /// let storage = MockBackend::new();
311 /// storage.put("temp.dat", b"temporary").await?;
312 ///
313 /// storage.delete("temp.dat").await?;
314 /// assert!(!storage.exists("temp.dat").await?);
315 ///
316 /// // Deleting again should succeed (idempotent)
317 /// storage.delete("temp.dat").await?;
318 /// # Ok(())
319 /// # }
320 /// ```
321 async fn delete(&self, key: &str) -> anyhow::Result<()>;
322
323 /// List objects with a given prefix
324 ///
325 /// Returns a sorted list of all keys that start with the given prefix.
326 /// Useful for organization and bulk operations.
327 ///
328 /// # Arguments
329 ///
330 /// * `prefix` - The key prefix to filter by (can be empty to list all)
331 ///
332 /// # Returns
333 ///
334 /// * `Ok(Vec<String>)` - Sorted list of matching keys (can be empty)
335 /// * `Err` - If an I/O error occurs or permission is denied
336 ///
337 /// # Errors
338 ///
339 /// Returns an error if:
340 /// - An I/O error occurs
341 /// - Permission is denied
342 ///
343 /// # Implementation Notes
344 ///
345 /// - Results should be sorted alphabetically for consistency
346 /// - An empty prefix should return all keys
347 /// - No keys should return an empty vec, not an error
348 /// - Prefix matching should be exact string prefix matching
349 ///
350 /// # Examples
351 ///
352 /// ```rust,no_run
353 /// # use mediagit_storage::{StorageBackend, mock::MockBackend};
354 /// # #[tokio::main]
355 /// # async fn main() -> anyhow::Result<()> {
356 /// let storage = MockBackend::new();
357 /// storage.put("images/photo1.jpg", b"data").await?;
358 /// storage.put("images/photo2.jpg", b"data").await?;
359 /// storage.put("videos/video1.mp4", b"data").await?;
360 ///
361 /// let images = storage.list_objects("images/").await?;
362 /// assert_eq!(images.len(), 2);
363 ///
364 /// let all = storage.list_objects("").await?;
365 /// assert_eq!(all.len(), 3);
366 /// # Ok(())
367 /// # }
368 /// ```
369 async fn list_objects(&self, prefix: &str) -> anyhow::Result<Vec<String>>;
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn storage_trait_compiles() {
378 // Compile-time verification that the trait is properly defined
379 // This test ensures the trait definition is syntactically correct
380 }
381
382 #[test]
383 fn trait_is_object_safe() {
384 // Verify the trait can be used as a trait object
385 fn _check_object_safe(_: &dyn StorageBackend) {}
386 }
387}