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}