acton_htmx/storage/
traits.rs

1//! File storage trait definitions
2
3use super::types::{StorageResult, StoredFile, UploadedFile};
4use async_trait::async_trait;
5
6/// Abstraction for file storage backends
7///
8/// This trait provides a unified interface for storing, retrieving, and managing files
9/// across different storage backends (local filesystem, S3, Azure Blob, etc.).
10///
11/// # Design Principles
12///
13/// - **Backend Agnostic**: Handlers don't need to know about storage implementation
14/// - **Async First**: All operations are async for optimal I/O performance
15/// - **Type Safe**: Strong types prevent common errors
16/// - **Production Ready**: Built-in support for streaming, validation, and error handling
17///
18/// # Implementation Requirements
19///
20/// Implementations must:
21/// - Generate unique identifiers for stored files (UUIDs recommended)
22/// - Handle concurrent access safely
23/// - Provide atomic operations where possible
24/// - Clean up resources on errors
25///
26/// # Examples
27///
28/// ```rust,no_run
29/// use acton_htmx::storage::{FileStorage, LocalFileStorage, UploadedFile};
30/// use std::path::PathBuf;
31///
32/// # async fn example() -> anyhow::Result<()> {
33/// // Create storage backend
34/// let storage = LocalFileStorage::new(PathBuf::from("/var/uploads"))?;
35///
36/// // Store a file
37/// let file = UploadedFile::new("avatar.png", "image/png", vec![/* ... */]);
38/// let stored = storage.store(file).await?;
39///
40/// // Retrieve the file
41/// let data = storage.retrieve(&stored.id).await?;
42///
43/// // Get file URL (for serving to clients)
44/// let url = storage.url(&stored.id).await?;
45///
46/// // Delete when no longer needed
47/// storage.delete(&stored.id).await?;
48/// # Ok(())
49/// # }
50/// ```
51#[cfg_attr(test, mockall::automock)]
52#[async_trait]
53pub trait FileStorage: Send + Sync {
54    /// Stores an uploaded file and returns metadata about the stored file
55    ///
56    /// This method should:
57    /// - Generate a unique ID for the file
58    /// - Persist the file data to the storage backend
59    /// - Return metadata that can be used to retrieve the file later
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if:
64    /// - The storage backend is unavailable
65    /// - There's insufficient storage space
66    /// - File I/O fails
67    ///
68    /// # Examples
69    ///
70    /// ```rust,no_run
71    /// # use acton_htmx::storage::{FileStorage, LocalFileStorage, UploadedFile};
72    /// # use std::path::PathBuf;
73    /// # async fn example() -> anyhow::Result<()> {
74    /// # let storage = LocalFileStorage::new(PathBuf::from("/tmp"))?;
75    /// let file = UploadedFile::new("report.pdf", "application/pdf", vec![/* ... */]);
76    /// let stored = storage.store(file).await?;
77    /// println!("Stored with ID: {}", stored.id);
78    /// # Ok(())
79    /// # }
80    /// ```
81    async fn store(&self, file: UploadedFile) -> StorageResult<StoredFile>;
82
83    /// Retrieves file data by ID
84    ///
85    /// # Errors
86    ///
87    /// Returns an error if:
88    /// - The file doesn't exist (`StorageError::NotFound`)
89    /// - The storage backend is unavailable
90    /// - File I/O fails
91    ///
92    /// # Examples
93    ///
94    /// ```rust,no_run
95    /// # use acton_htmx::storage::{FileStorage, LocalFileStorage};
96    /// # use std::path::PathBuf;
97    /// # async fn example() -> anyhow::Result<()> {
98    /// # let storage = LocalFileStorage::new(PathBuf::from("/tmp"))?;
99    /// let data = storage.retrieve("550e8400-e29b-41d4-a716-446655440000").await?;
100    /// println!("Retrieved {} bytes", data.len());
101    /// # Ok(())
102    /// # }
103    /// ```
104    async fn retrieve(&self, id: &str) -> StorageResult<Vec<u8>>;
105
106    /// Deletes a file by ID
107    ///
108    /// This operation should be idempotent - deleting a non-existent file
109    /// should not return an error.
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if:
114    /// - The storage backend is unavailable
115    /// - File I/O fails (permissions, etc.)
116    ///
117    /// # Examples
118    ///
119    /// ```rust,no_run
120    /// # use acton_htmx::storage::{FileStorage, LocalFileStorage};
121    /// # use std::path::PathBuf;
122    /// # async fn example() -> anyhow::Result<()> {
123    /// # let storage = LocalFileStorage::new(PathBuf::from("/tmp"))?;
124    /// storage.delete("550e8400-e29b-41d4-a716-446655440000").await?;
125    /// println!("File deleted successfully");
126    /// # Ok(())
127    /// # }
128    /// ```
129    async fn delete(&self, id: &str) -> StorageResult<()>;
130
131    /// Returns a URL for accessing the file
132    ///
133    /// The returned URL format depends on the storage backend:
134    /// - Local storage: relative path (e.g., "/uploads/abc123/file.jpg")
135    /// - S3: presigned URL or public URL
136    /// - CDN: CDN URL
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if:
141    /// - The file doesn't exist
142    /// - URL generation fails (e.g., S3 presigning error)
143    ///
144    /// # Examples
145    ///
146    /// ```rust,no_run
147    /// # use acton_htmx::storage::{FileStorage, LocalFileStorage};
148    /// # use std::path::PathBuf;
149    /// # async fn example() -> anyhow::Result<()> {
150    /// # let storage = LocalFileStorage::new(PathBuf::from("/tmp"))?;
151    /// let url = storage.url("550e8400-e29b-41d4-a716-446655440000").await?;
152    /// println!("File available at: {}", url);
153    /// # Ok(())
154    /// # }
155    /// ```
156    async fn url(&self, id: &str) -> StorageResult<String>;
157
158    /// Checks if a file exists
159    ///
160    /// This is useful for validating file references before attempting retrieval.
161    ///
162    /// # Examples
163    ///
164    /// ```rust,no_run
165    /// # use acton_htmx::storage::{FileStorage, LocalFileStorage};
166    /// # use std::path::PathBuf;
167    /// # async fn example() -> anyhow::Result<()> {
168    /// # let storage = LocalFileStorage::new(PathBuf::from("/tmp"))?;
169    /// if storage.exists("550e8400-e29b-41d4-a716-446655440000").await? {
170    ///     println!("File exists!");
171    /// }
172    /// # Ok(())
173    /// # }
174    /// ```
175    async fn exists(&self, id: &str) -> StorageResult<bool>;
176
177    /// Retrieves file metadata by ID
178    ///
179    /// This method retrieves only the metadata (filename, content type, size, etc.)
180    /// without reading the actual file data. This is useful for serving files with
181    /// proper Content-Type headers and other metadata.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if:
186    /// - The file doesn't exist (`StorageError::NotFound`)
187    /// - The storage backend is unavailable
188    /// - Metadata cannot be read
189    ///
190    /// # Examples
191    ///
192    /// ```rust,no_run
193    /// # use acton_htmx::storage::{FileStorage, LocalFileStorage};
194    /// # use std::path::PathBuf;
195    /// # async fn example() -> anyhow::Result<()> {
196    /// # let storage = LocalFileStorage::new(PathBuf::from("/tmp"))?;
197    /// let metadata = storage.get_metadata("550e8400-e29b-41d4-a716-446655440000").await?;
198    /// println!("Content-Type: {}", metadata.content_type);
199    /// println!("Size: {} bytes", metadata.size);
200    /// # Ok(())
201    /// # }
202    /// ```
203    async fn get_metadata(&self, id: &str) -> StorageResult<StoredFile>;
204}