ferro_storage/drivers/
local.rs1use crate::storage::{FileMetadata, PutOptions, StorageDriver};
4use crate::Error;
5use async_trait::async_trait;
6use bytes::Bytes;
7use std::path::{Path, PathBuf};
8use tokio::fs;
9use tracing::debug;
10
11pub struct LocalDriver {
13 root: PathBuf,
15 url_base: Option<String>,
17}
18
19impl LocalDriver {
20 pub fn new(root: impl AsRef<Path>) -> Self {
22 Self {
23 root: root.as_ref().to_path_buf(),
24 url_base: None,
25 }
26 }
27
28 pub fn with_url_base(mut self, url: impl Into<String>) -> Self {
30 self.url_base = Some(url.into());
31 self
32 }
33
34 fn full_path(&self, path: &str) -> PathBuf {
36 self.root.join(path)
37 }
38
39 async fn ensure_directory(&self, path: &Path) -> Result<(), Error> {
41 if let Some(parent) = path.parent() {
42 fs::create_dir_all(parent).await?;
43 }
44 Ok(())
45 }
46}
47
48#[async_trait]
49impl StorageDriver for LocalDriver {
50 async fn exists(&self, path: &str) -> Result<bool, Error> {
51 let full_path = self.full_path(path);
52 Ok(full_path.exists())
53 }
54
55 async fn get(&self, path: &str) -> Result<Bytes, Error> {
56 let full_path = self.full_path(path);
57 debug!(path = %full_path.display(), "Reading file");
58
59 let contents = fs::read(&full_path).await.map_err(|e| {
60 if e.kind() == std::io::ErrorKind::NotFound {
61 Error::not_found(path)
62 } else {
63 Error::from(e)
64 }
65 })?;
66
67 Ok(Bytes::from(contents))
68 }
69
70 async fn put(&self, path: &str, contents: Bytes, _options: PutOptions) -> Result<(), Error> {
71 let full_path = self.full_path(path);
72 debug!(path = %full_path.display(), "Writing file");
73
74 self.ensure_directory(&full_path).await?;
75 fs::write(&full_path, &contents).await?;
76
77 Ok(())
78 }
79
80 async fn delete(&self, path: &str) -> Result<(), Error> {
81 let full_path = self.full_path(path);
82 debug!(path = %full_path.display(), "Deleting file");
83
84 fs::remove_file(&full_path).await.map_err(|e| {
85 if e.kind() == std::io::ErrorKind::NotFound {
86 Error::not_found(path)
87 } else {
88 Error::from(e)
89 }
90 })
91 }
92
93 async fn copy(&self, from: &str, to: &str) -> Result<(), Error> {
94 let from_path = self.full_path(from);
95 let to_path = self.full_path(to);
96
97 debug!(from = %from_path.display(), to = %to_path.display(), "Copying file");
98
99 self.ensure_directory(&to_path).await?;
100 fs::copy(&from_path, &to_path).await.map_err(|e| {
101 if e.kind() == std::io::ErrorKind::NotFound {
102 Error::not_found(from)
103 } else {
104 Error::from(e)
105 }
106 })?;
107
108 Ok(())
109 }
110
111 async fn size(&self, path: &str) -> Result<u64, Error> {
112 let full_path = self.full_path(path);
113 let metadata = fs::metadata(&full_path).await.map_err(|e| {
114 if e.kind() == std::io::ErrorKind::NotFound {
115 Error::not_found(path)
116 } else {
117 Error::from(e)
118 }
119 })?;
120
121 Ok(metadata.len())
122 }
123
124 async fn metadata(&self, path: &str) -> Result<FileMetadata, Error> {
125 let full_path = self.full_path(path);
126 let fs_meta = fs::metadata(&full_path).await.map_err(|e| {
127 if e.kind() == std::io::ErrorKind::NotFound {
128 Error::not_found(path)
129 } else {
130 Error::from(e)
131 }
132 })?;
133
134 let mime_type = mime_guess::from_path(&full_path)
135 .first()
136 .map(|m| m.to_string());
137
138 let mut meta = FileMetadata::new(path, fs_meta.len());
139
140 if let Ok(modified) = fs_meta.modified() {
141 meta = meta.with_last_modified(modified);
142 }
143
144 if let Some(mime) = mime_type {
145 meta = meta.with_mime_type(mime);
146 }
147
148 Ok(meta)
149 }
150
151 async fn url(&self, path: &str) -> Result<String, Error> {
152 match &self.url_base {
153 Some(base) => Ok(format!("{}/{}", base.trim_end_matches('/'), path)),
154 None => Ok(self.full_path(path).to_string_lossy().to_string()),
155 }
156 }
157
158 async fn temporary_url(
159 &self,
160 path: &str,
161 _expiration: std::time::Duration,
162 ) -> Result<String, Error> {
163 self.url(path).await
165 }
166
167 async fn files(&self, directory: &str) -> Result<Vec<String>, Error> {
168 let full_path = self.full_path(directory);
169 let mut files = Vec::new();
170
171 if !full_path.exists() {
172 return Ok(files);
173 }
174
175 let mut entries = fs::read_dir(&full_path).await?;
176 while let Some(entry) = entries.next_entry().await? {
177 let path = entry.path();
178 if path.is_file() {
179 if let Some(name) = path.file_name() {
180 files.push(name.to_string_lossy().to_string());
181 }
182 }
183 }
184
185 Ok(files)
186 }
187
188 async fn all_files(&self, directory: &str) -> Result<Vec<String>, Error> {
189 let full_path = self.full_path(directory);
190 let mut files = Vec::new();
191
192 if !full_path.exists() {
193 return Ok(files);
194 }
195
196 self.collect_files_recursive(&full_path, &full_path, &mut files)
197 .await?;
198 Ok(files)
199 }
200
201 async fn directories(&self, directory: &str) -> Result<Vec<String>, Error> {
202 let full_path = self.full_path(directory);
203 let mut dirs = Vec::new();
204
205 if !full_path.exists() {
206 return Ok(dirs);
207 }
208
209 let mut entries = fs::read_dir(&full_path).await?;
210 while let Some(entry) = entries.next_entry().await? {
211 let path = entry.path();
212 if path.is_dir() {
213 if let Some(name) = path.file_name() {
214 dirs.push(name.to_string_lossy().to_string());
215 }
216 }
217 }
218
219 Ok(dirs)
220 }
221
222 async fn make_directory(&self, path: &str) -> Result<(), Error> {
223 let full_path = self.full_path(path);
224 fs::create_dir_all(&full_path).await?;
225 Ok(())
226 }
227
228 async fn delete_directory(&self, path: &str) -> Result<(), Error> {
229 let full_path = self.full_path(path);
230 fs::remove_dir_all(&full_path).await.map_err(|e| {
231 if e.kind() == std::io::ErrorKind::NotFound {
232 Error::not_found(path)
233 } else {
234 Error::from(e)
235 }
236 })
237 }
238}
239
240impl LocalDriver {
241 #[allow(clippy::only_used_in_recursion)]
242 fn collect_files_recursive<'a>(
243 &'a self,
244 base: &'a Path,
245 current: &'a Path,
246 files: &'a mut Vec<String>,
247 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), Error>> + Send + 'a>> {
248 Box::pin(async move {
249 let mut entries = fs::read_dir(current).await?;
250 while let Some(entry) = entries.next_entry().await? {
251 let path = entry.path();
252 if path.is_file() {
253 if let Ok(relative) = path.strip_prefix(base) {
254 files.push(relative.to_string_lossy().to_string());
255 }
256 } else if path.is_dir() {
257 self.collect_files_recursive(base, &path, files).await?;
258 }
259 }
260 Ok(())
261 })
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[tokio::test]
270 async fn test_local_driver_put_get() {
271 let temp_dir = tempfile::tempdir().unwrap();
272 let driver = LocalDriver::new(temp_dir.path());
273
274 driver
275 .put("test.txt", Bytes::from("hello world"), PutOptions::new())
276 .await
277 .unwrap();
278
279 let contents = driver.get("test.txt").await.unwrap();
280 assert_eq!(contents, Bytes::from("hello world"));
281 }
282
283 #[tokio::test]
284 async fn test_local_driver_exists() {
285 let temp_dir = tempfile::tempdir().unwrap();
286 let driver = LocalDriver::new(temp_dir.path());
287
288 assert!(!driver.exists("missing.txt").await.unwrap());
289
290 driver
291 .put("exists.txt", Bytes::from("data"), PutOptions::new())
292 .await
293 .unwrap();
294
295 assert!(driver.exists("exists.txt").await.unwrap());
296 }
297
298 #[tokio::test]
299 async fn test_local_driver_delete() {
300 let temp_dir = tempfile::tempdir().unwrap();
301 let driver = LocalDriver::new(temp_dir.path());
302
303 driver
304 .put("to_delete.txt", Bytes::from("data"), PutOptions::new())
305 .await
306 .unwrap();
307
308 driver.delete("to_delete.txt").await.unwrap();
309 assert!(!driver.exists("to_delete.txt").await.unwrap());
310 }
311
312 #[tokio::test]
313 async fn test_local_driver_copy() {
314 let temp_dir = tempfile::tempdir().unwrap();
315 let driver = LocalDriver::new(temp_dir.path());
316
317 driver
318 .put(
319 "original.txt",
320 Bytes::from("original content"),
321 PutOptions::new(),
322 )
323 .await
324 .unwrap();
325
326 driver.copy("original.txt", "copy.txt").await.unwrap();
327
328 let contents = driver.get("copy.txt").await.unwrap();
329 assert_eq!(contents, Bytes::from("original content"));
330 }
331
332 #[tokio::test]
333 async fn test_local_driver_nested_directories() {
334 let temp_dir = tempfile::tempdir().unwrap();
335 let driver = LocalDriver::new(temp_dir.path());
336
337 driver
338 .put(
339 "a/b/c/deep.txt",
340 Bytes::from("deep content"),
341 PutOptions::new(),
342 )
343 .await
344 .unwrap();
345
346 let contents = driver.get("a/b/c/deep.txt").await.unwrap();
347 assert_eq!(contents, Bytes::from("deep content"));
348 }
349
350 #[tokio::test]
351 async fn test_local_driver_url() {
352 let temp_dir = tempfile::tempdir().unwrap();
353 let driver = LocalDriver::new(temp_dir.path()).with_url_base("https://example.com/storage");
354
355 let url = driver.url("images/photo.jpg").await.unwrap();
356 assert_eq!(url, "https://example.com/storage/images/photo.jpg");
357 }
358}