Skip to main content

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}